php version manager attempt #1

This commit is contained in:
Alex
2025-12-29 00:16:01 +02:00
parent 144d8eefac
commit 3d0d10ea41
9 changed files with 627 additions and 3 deletions

View File

@@ -15,4 +15,153 @@ class PHPManagerController extends Controller
return response()->json($versions);
}
/**
* Render the PHP management page
*/
public function index()
{
return inertia('PHP/Index');
}
/**
* List all installed PHP versions with their systemctl status
*/
public function list(): JsonResponse
{
$scriptPath = base_path('laranode-scripts/bin/laranode-php-list.sh');
$output = shell_exec("sudo bash {$scriptPath}");
$phpVersions = json_decode($output, true) ?? [];
return response()->json($phpVersions);
}
/**
* Install a new PHP version
*/
public function install(Request $request): JsonResponse
{
$request->validate([
'version' => 'required|string|regex:/^\d+\.\d+$/',
]);
$version = $request->input('version');
$scriptPath = base_path('laranode-scripts/bin/laranode-php-install.sh');
// Execute installation script
$output = shell_exec("sudo bash {$scriptPath} {$version} 2>&1");
// Check if installation was successful
if (strpos($output, 'installed successfully') !== false) {
return response()->json([
'success' => true,
'message' => "PHP {$version} installed successfully",
'output' => $output
]);
}
return response()->json([
'success' => false,
'message' => "Failed to install PHP {$version}",
'output' => $output
], 500);
}
/**
* Uninstall a PHP version
*/
public function uninstall(Request $request): JsonResponse
{
$request->validate([
'version' => 'required|string|regex:/^\d+\.\d+$/',
]);
$version = $request->input('version');
$scriptPath = base_path('laranode-scripts/bin/laranode-php-uninstall.sh');
// Execute uninstallation script
$output = shell_exec("sudo bash {$scriptPath} {$version} 2>&1");
// Check if uninstallation was successful
if (strpos($output, 'uninstalled successfully') !== false) {
return response()->json([
'success' => true,
'message' => "PHP {$version} uninstalled successfully",
'output' => $output
]);
}
return response()->json([
'success' => false,
'message' => "Failed to uninstall PHP {$version}",
'output' => $output
], 500);
}
/**
* Toggle PHP-FPM service (enable/disable)
*/
public function toggleService(Request $request): JsonResponse
{
$request->validate([
'version' => 'required|string|regex:/^\d+\.\d+$/',
'enabled' => 'required|boolean',
]);
$version = $request->input('version');
$enabled = $request->input('enabled');
$action = $enabled ? 'enable' : 'disable';
$scriptPath = base_path('laranode-scripts/bin/laranode-php-service.sh');
// Execute service management script
$output = shell_exec("sudo bash {$scriptPath} {$action} {$version} 2>&1");
// Check if action was successful
if (strpos($output, 'completed successfully') !== false) {
return response()->json([
'success' => true,
'message' => "PHP {$version}-FPM service {$action}d successfully",
'output' => $output
]);
}
return response()->json([
'success' => false,
'message' => "Failed to {$action} PHP {$version}-FPM service",
'output' => $output
], 500);
}
/**
* Restart PHP-FPM service
*/
public function restartService(Request $request): JsonResponse
{
$request->validate([
'version' => 'required|string|regex:/^\d+\.\d+$/',
]);
$version = $request->input('version');
$scriptPath = base_path('laranode-scripts/bin/laranode-php-service.sh');
// Execute restart script
$output = shell_exec("sudo bash {$scriptPath} restart {$version} 2>&1");
// Check if restart was successful
if (strpos($output, 'completed successfully') !== false) {
return response()->json([
'success' => true,
'message' => "PHP {$version}-FPM service restarted successfully",
'output' => $output
]);
}
return response()->json([
'success' => false,
'message' => "Failed to restart PHP {$version}-FPM service",
'output' => $output
], 500);
}
}

View File

@@ -0,0 +1,47 @@
#!/bin/bash
# Install a PHP-FPM version with common extensions
# Usage: ./laranode-php-install.sh {version}
# Example: ./laranode-php-install.sh 8.4
if [ $# -lt 1 ]; then
echo "Usage: $0 {php version: example 8.4}"
exit 1
fi
PHP_VERSION=$1
echo "Installing PHP $PHP_VERSION-FPM..."
# Update apt cache
apt-get update -qq
# Install PHP-FPM and common extensions
apt-get install -y \
php${PHP_VERSION}-fpm \
php${PHP_VERSION}-cli \
php${PHP_VERSION}-common \
php${PHP_VERSION}-mysql \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-curl \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-zip \
php${PHP_VERSION}-gd \
php${PHP_VERSION}-bcmath \
php${PHP_VERSION}-intl
if [ $? -eq 0 ]; then
echo "PHP $PHP_VERSION installed successfully"
# Enable the service
systemctl enable php${PHP_VERSION}-fpm
# Start the service
systemctl start php${PHP_VERSION}-fpm
echo "PHP $PHP_VERSION-FPM service enabled and started"
exit 0
else
echo "Failed to install PHP $PHP_VERSION"
exit 1
fi

View File

@@ -0,0 +1,38 @@
#!/bin/bash
# List all installed PHP-FPM versions with their systemctl status
# Output: JSON array of PHP versions with status
# Get all installed php*-fpm packages
php_versions=$(dpkg -l | grep -E 'php[0-9]+\.[0-9]+-fpm' | awk '{print $2}' | sed 's/php\(.*\)-fpm/\1/')
echo "["
first=true
for version in $php_versions; do
# Get systemctl status
if systemctl is-active --quiet "php${version}-fpm"; then
status="active"
else
status="inactive"
fi
# Get systemctl enabled status
if systemctl is-enabled --quiet "php${version}-fpm" 2>/dev/null; then
enabled="true"
else
enabled="false"
fi
# Output JSON object
if [ "$first" = true ]; then
first=false
else
echo ","
fi
echo -n " {\"version\": \"$version\", \"status\": \"$status\", \"enabled\": $enabled}"
done
echo ""
echo "]"

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# Manage PHP-FPM service (enable/disable/restart)
# Usage: ./laranode-php-service.sh {action} {version}
# Example: ./laranode-php-service.sh enable 8.4
if [ $# -lt 2 ]; then
echo "Usage: $0 {action: enable|disable|restart} {php version: example 8.4}"
exit 1
fi
ACTION=$1
PHP_VERSION=$2
case $ACTION in
enable)
echo "Enabling PHP $PHP_VERSION-FPM service..."
systemctl enable php${PHP_VERSION}-fpm
systemctl start php${PHP_VERSION}-fpm
;;
disable)
echo "Disabling PHP $PHP_VERSION-FPM service..."
systemctl stop php${PHP_VERSION}-fpm
systemctl disable php${PHP_VERSION}-fpm
;;
restart)
echo "Restarting PHP $PHP_VERSION-FPM service..."
systemctl restart php${PHP_VERSION}-fpm
;;
*)
echo "Invalid action: $ACTION"
echo "Valid actions: enable, disable, restart"
exit 1
;;
esac
if [ $? -eq 0 ]; then
echo "Action '$ACTION' completed successfully for PHP $PHP_VERSION-FPM"
exit 0
else
echo "Failed to $ACTION PHP $PHP_VERSION-FPM"
exit 1
fi

View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Uninstall a PHP-FPM version
# Usage: ./laranode-php-uninstall.sh {version}
# Example: ./laranode-php-uninstall.sh 8.4
if [ $# -lt 1 ]; then
echo "Usage: $0 {php version: example 8.4}"
exit 1
fi
PHP_VERSION=$1
echo "Uninstalling PHP $PHP_VERSION-FPM..."
# Stop the service
systemctl stop php${PHP_VERSION}-fpm
# Disable the service
systemctl disable php${PHP_VERSION}-fpm
# Remove PHP packages
apt-get remove -y php${PHP_VERSION}-*
# Purge configuration files
apt-get purge -y php${PHP_VERSION}-*
# Clean up
apt-get autoremove -y
if [ $? -eq 0 ]; then
echo "PHP $PHP_VERSION uninstalled successfully"
exit 0
else
echo "Failed to uninstall PHP $PHP_VERSION"
exit 1
fi

View File

@@ -107,11 +107,10 @@ const SidebarNavi = () => {
</Link>
</li>
{/* Postponed for next release
{auth.user.role == 'admin' && (
<li>
<Link
href="/php-manager"
href={route('php.index')}
className="relative flex flex-row items-center h-11 focus:outline-none hover:bg-gray-900 text-gray-300 border-l-4 border-transparent hover:border-indigo-900 pr-6"
>
<div>
@@ -121,7 +120,6 @@ const SidebarNavi = () => {
</Link>
</li>
)}
*/}
<li>
<Link

View File

@@ -0,0 +1,212 @@
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, router, usePage } from '@inertiajs/react';
import { TbBrandPhp } from 'react-icons/tb';
import { TiDelete } from 'react-icons/ti';
import { FaToggleOn, FaToggleOff, FaSync } from 'react-icons/fa';
import { toast } from 'react-toastify';
import InstallPHPForm from './Partials/InstallPHPForm';
import ConfirmationButton from '@/Components/ConfirmationButton';
import { useEffect, useState } from 'react';
export default function PHPIndex() {
const { auth } = usePage().props;
const [phpVersions, setPhpVersions] = useState([]);
const [liveStats, setLiveStats] = useState({});
const [loading, setLoading] = useState(true);
const echo = window.Echo;
// Fetch installed PHP versions
const fetchPhpVersions = () => {
fetch(route('php.list'), {
headers: {
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
setPhpVersions(data);
setLoading(false);
})
.catch(() => {
toast.error('Failed to fetch PHP versions');
setLoading(false);
});
};
useEffect(() => {
fetchPhpVersions();
// Subscribe to live stats
const dashboardChannel = echo.private("systemstats");
dashboardChannel.listen("SystemStatsEvent", (data) => {
if (data.phpFpm) {
setLiveStats(data.phpFpm);
}
});
const whisperInterval = setInterval(() => {
dashboardChannel.whisper("typing", { requesting: "dashboard-realtime-stats" });
}, 2000);
return () => {
clearInterval(whisperInterval);
echo.leave("systemstats");
};
}, []);
const uninstallPhp = (version) => {
router.delete(route('php.uninstall'), {
data: { version },
onBefore: () => toast('Uninstalling PHP ' + version + '...'),
onSuccess: () => {
toast.success('PHP ' + version + ' uninstalled successfully');
fetchPhpVersions();
},
onError: () => toast.error('Failed to uninstall PHP ' + version),
});
};
const toggleService = (version, currentEnabled) => {
const enabled = !currentEnabled;
const action = enabled ? 'enable' : 'disable';
router.post(route('php.service.toggle'),
{ version, enabled },
{
onBefore: () => toast(`${action === 'enable' ? 'Enabling' : 'Disabling'} PHP ${version}-FPM...`),
onSuccess: () => {
toast.success(`PHP ${version}-FPM ${action}d successfully`);
fetchPhpVersions();
},
onError: () => toast.error(`Failed to ${action} PHP ${version}-FPM`),
}
);
};
const restartService = (version) => {
router.post(route('php.service.restart'),
{ version },
{
onBefore: () => toast('Restarting PHP ' + version + '-FPM...'),
onSuccess: () => {
toast.success('PHP ' + version + '-FPM restarted successfully');
fetchPhpVersions();
},
onError: () => toast.error('Failed to restart PHP ' + version + '-FPM'),
}
);
};
return (
<AuthenticatedLayout
header={
<div className="flex flex-col xl:flex-row xl:justify-between max-w-7xl pr-5">
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight flex items-center">
<TbBrandPhp className='mr-2' />
PHP Versions
</h2>
<InstallPHPForm />
</div>
}
>
<Head title="PHP Versions" />
<div className="max-w-7xl px-4 my-8">
{loading ? (
<div className="text-center py-8 text-gray-600 dark:text-gray-400">
Loading PHP versions...
</div>
) : (
<div className="relative overflow-x-auto bg-white dark:bg-gray-850 mt-3">
<table className="w-full text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead className="text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300 text-sm">
<tr>
<th className="px-6 py-3">Version</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Memory</th>
<th className="px-6 py-3">CPU Time</th>
<th className="px-6 py-3">Uptime</th>
<th className="px-6 py-3">Actions</th>
</tr>
</thead>
<tbody className="text-sm">
{phpVersions.length === 0 ? (
<tr>
<td colSpan="6" className="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
No PHP versions installed
</td>
</tr>
) : (
phpVersions.map((php, index) => {
const stats = liveStats[php.version] || {};
return (
<tr key={`php-${index}`} className="bg-white border-b text-gray-700 dark:text-gray-200 dark:bg-gray-850 dark:border-gray-700 border-gray-200">
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<div className="flex items-center">
<TbBrandPhp className="w-5 h-5 mr-2 text-blue-600" />
PHP {php.version}
</div>
</td>
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<div className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${php.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
}`}>
{php.status === 'active' ? 'Active' : 'Inactive'}
</div>
</td>
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{stats.memory || '--'}
</td>
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{stats.cpuTime || '--'}
</td>
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{stats.uptime || '--'}
</td>
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<div className='flex items-center space-x-2'>
<ConfirmationButton doAction={() => toggleService(php.version, php.enabled)}>
<button
className={`p-2 rounded-lg transition-colors ${php.enabled
? 'bg-green-100 hover:bg-green-200 text-green-600 dark:bg-green-900 dark:hover:bg-green-800 dark:text-green-300'
: 'bg-gray-100 hover:bg-gray-200 text-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-400'
}`}
title={php.enabled ? 'Disable Service' : 'Enable Service'}
>
{php.enabled ? (
<FaToggleOn className='w-5 h-5' />
) : (
<FaToggleOff className='w-5 h-5' />
)}
</button>
</ConfirmationButton>
<ConfirmationButton doAction={() => restartService(php.version)}>
<button
className="p-2 rounded-lg bg-blue-100 hover:bg-blue-200 text-blue-600 dark:bg-blue-900 dark:hover:bg-blue-800 dark:text-blue-300 transition-colors"
title="Restart Service"
>
<FaSync className='w-4 h-4' />
</button>
</ConfirmationButton>
<ConfirmationButton doAction={() => uninstallPhp(php.version)}>
<TiDelete className='w-6 h-6 text-red-500' />
</ConfirmationButton>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
)}
</div>
</AuthenticatedLayout>
);
}

View File

@@ -0,0 +1,92 @@
import { useState } from 'react';
import Modal from '@/Components/Modal';
import PrimaryButton from '@/Components/PrimaryButton';
import SecondaryButton from '@/Components/SecondaryButton';
import { router } from '@inertiajs/react';
import { toast } from 'react-toastify';
import { TbBrandPhp } from 'react-icons/tb';
export default function InstallPHPForm() {
const [showModal, setShowModal] = useState(false);
const [version, setVersion] = useState('');
const [isInstalling, setIsInstalling] = useState(false);
const availableVersions = ['8.4', '8.3', '8.2', '8.1', '8.0', '7.4'];
const handleInstall = () => {
if (!version) {
toast.error('Please select a PHP version');
return;
}
setIsInstalling(true);
router.post(route('php.install'),
{ version },
{
onBefore: () => toast('Installing PHP ' + version + '...'),
onSuccess: () => {
toast.success('PHP ' + version + ' installed successfully');
setShowModal(false);
setVersion('');
router.reload();
},
onError: (errors) => {
toast.error('Failed to install PHP ' + version);
console.error(errors);
},
onFinish: () => setIsInstalling(false),
}
);
};
return (
<>
<PrimaryButton onClick={() => setShowModal(true)}>
<TbBrandPhp className="mr-2" />
Install New Version
</PrimaryButton>
<Modal show={showModal} onClose={() => !isInstalling && setShowModal(false)}>
<div className="p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Install PHP Version
</h2>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
Select a PHP version to install. This will install PHP-FPM and common extensions.
</p>
<div className="mt-6">
<label htmlFor="php-version" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
PHP Version
</label>
<select
id="php-version"
value={version}
onChange={(e) => setVersion(e.target.value)}
disabled={isInstalling}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-white sm:text-sm"
>
<option value="">Select a version</option>
{availableVersions.map((v) => (
<option key={v} value={v}>
PHP {v}
</option>
))}
</select>
</div>
<div className="mt-6 flex justify-end space-x-3">
<SecondaryButton onClick={() => setShowModal(false)} disabled={isInstalling}>
Cancel
</SecondaryButton>
<PrimaryButton onClick={handleInstall} disabled={isInstalling || !version}>
{isInstalling ? 'Installing...' : 'Install'}
</PrimaryButton>
</div>
</div>
</Modal>
</>
);
}

View File

@@ -35,7 +35,15 @@ Route::post('/websites/{website}/ssl/toggle', [WebsiteController::class, 'toggle
Route::get('/websites/{website}/ssl/status', [WebsiteController::class, 'checkSslStatus'])->middleware(['auth'])->name('websites.ssl.status');
// PHP FPM Pools [Admin | User]
Route::get('/php', [PHPManagerController::class, 'index'])->middleware(['auth', AdminMiddleware::class])->name('php.index');
Route::get('/php/get-versions', [PHPManagerController::class, 'getVersions'])->middleware(['auth'])->name('php.get-versions');
Route::get('/php/list', [PHPManagerController::class, 'list'])->middleware(['auth', AdminMiddleware::class])->name('php.list');
Route::post('/php/install', [PHPManagerController::class, 'install'])->middleware(['auth', AdminMiddleware::class])->name('php.install');
Route::delete('/php/uninstall', [PHPManagerController::class, 'uninstall'])->middleware(['auth', AdminMiddleware::class])->name('php.uninstall');
Route::post('/php/service/toggle', [PHPManagerController::class, 'toggleService'])->middleware(['auth', AdminMiddleware::class])->name('php.service.toggle');
Route::post('/php/service/restart', [PHPManagerController::class, 'restartService'])->middleware(['auth', AdminMiddleware::class])->name('php.service.restart');
// MySQL management [Admin | User]
Route::get('/mysql', [MysqlController::class, 'index'])->middleware(['auth'])->name('mysql.index');