diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe3165..0981817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-02-18 +- Added CLI default PHP version detection to system status (`php_default_version`) using the CLI `php` binary. +- Updated ACP System -> CLI to show the CLI default PHP path/version in the panel header with sufficiency indicator and warning tooltip. +- Simplified ACP CLI PHP selector to `php` or custom binary, and blocked saving `keyhelp-php-domain` from ACP. +- Added test coverage expectation for `php_default_version` in system status unit tests. + ## 2026-02-12 - Refined ACP System tab with left navigation, section-specific requirements, and CLI PHP selector. - Added CLI PHP interpreter options (php, keyhelp-php-domain, custom) with KeyHelp guidance. diff --git a/app/Http/Controllers/SystemStatusController.php b/app/Http/Controllers/SystemStatusController.php index 13682a8..0b82377 100644 --- a/app/Http/Controllers/SystemStatusController.php +++ b/app/Http/Controllers/SystemStatusController.php @@ -17,6 +17,7 @@ class SystemStatusController extends Controller } $phpDefaultPath = $this->resolveBinary('php'); + $phpDefaultVersion = $phpDefaultPath ? $this->resolvePhpVersion($phpDefaultPath) : null; $phpConfiguredPath = trim((string) Setting::where('key', 'system.php_binary')->value('value')); $phpSelectedPath = $phpConfiguredPath ?: (PHP_BINARY ?: $phpDefaultPath); $phpSelectedOk = (bool) $phpSelectedPath; @@ -44,6 +45,7 @@ class SystemStatusController extends Controller return response()->json([ 'php' => PHP_VERSION, 'php_default' => $phpDefaultPath, + 'php_default_version' => $phpDefaultVersion, 'php_configured' => $phpConfiguredPath ?: null, 'php_selected_path' => $phpSelectedPath, 'php_selected_ok' => $phpSelectedOk, diff --git a/resources/js/pages/Acp.jsx b/resources/js/pages/Acp.jsx index d4997f5..152e431 100644 --- a/resources/js/pages/Acp.jsx +++ b/resources/js/pages/Acp.jsx @@ -63,6 +63,7 @@ const StatusIcon = ({ status = 'bad', tooltip }) => { } function Acp({ isAdmin }) { + const forcedMinPhpForTesting = '>=8.5' const { t } = useTranslation() const { roles: authRoles } = useAuth() const canManageFounder = authRoles.includes('ROLE_FOUNDER') @@ -299,11 +300,7 @@ function Acp({ isAdmin }) { } setGeneralSettings(next) const configuredPhp = settingsMap.get('system.php_binary') || '' - const phpMode = configuredPhp === 'keyhelp-php-domain' - ? 'keyhelp' - : configuredPhp === '' || configuredPhp === 'php' - ? 'php' - : 'custom' + const phpMode = configuredPhp === '' || configuredPhp === 'php' ? 'php' : 'custom' setSystemCliSettings({ php_mode: phpMode, php_custom: phpMode === 'custom' ? configuredPhp : '', @@ -399,11 +396,12 @@ function Acp({ isAdmin }) { value = typeof systemCliSettings.php_custom === 'string' ? systemCliSettings.php_custom.trim() : String(systemCliSettings.php_custom ?? '') - } else if (systemCliSettings.php_mode === 'keyhelp') { - value = 'keyhelp-php-domain' } else { value = 'php' } + if (value === 'keyhelp-php-domain') { + throw new Error('`keyhelp-php-domain` is disabled in ACP CLI settings. Use a custom binary (e.g. keyhelp-php84).') + } await saveSetting('system.php_binary', value) setSystemCliSettings((prev) => ({ ...prev, @@ -417,6 +415,67 @@ function Acp({ isAdmin }) { } } + const normalizeSemver = (value) => { + if (!value) return null + const match = String(value).trim().match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/) + if (!match) return null + return [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)] + } + + const compareSemver = (a, b) => { + for (let i = 0; i < 3; i += 1) { + if (a[i] > b[i]) return 1 + if (a[i] < b[i]) return -1 + } + return 0 + } + + const parseMinPhpConstraint = (constraint) => { + if (!constraint) return null + const parts = String(constraint) + .split('||') + .map((part) => part.trim()) + .filter(Boolean) + const mins = [] + + for (const part of parts) { + const tokens = part.split(/\s+/).filter(Boolean) + const geToken = tokens.find((token) => token.startsWith('>=')) + if (geToken) { + const parsed = normalizeSemver(geToken.slice(2)) + if (parsed) mins.push(parsed) + continue + } + + const caretToken = tokens.find((token) => token.startsWith('^')) + if (caretToken) { + const parsed = normalizeSemver(caretToken.slice(1)) + if (parsed) mins.push(parsed) + continue + } + + const tildeToken = tokens.find((token) => token.startsWith('~')) + if (tildeToken) { + const parsed = normalizeSemver(tildeToken.slice(1)) + if (parsed) mins.push(parsed) + continue + } + + const plain = normalizeSemver(tokens[0] || '') + if (plain) mins.push(plain) + } + + if (!mins.length) return null + return mins.reduce((lowest, current) => (compareSemver(current, lowest) < 0 ? current : lowest)) + } + + const cliDefaultPhpIsSufficient = useMemo(() => { + const current = normalizeSemver(systemStatus?.php_default_version) + const minimum = parseMinPhpConstraint(forcedMinPhpForTesting || systemStatus?.min_versions?.php) + if (!current || !minimum) return false + return compareSemver(current, minimum) >= 0 + }, [systemStatus, forcedMinPhpForTesting]) + const systemChecks = useMemo(() => { if (!systemStatus) return [] return [ @@ -424,7 +483,7 @@ function Acp({ isAdmin }) { id: 'php', label: 'PHP', path: systemStatus.php_selected_path || '—', - min: systemStatus.min_versions?.php || '—', + min: forcedMinPhpForTesting || systemStatus.min_versions?.php || '—', current: systemStatus.php_selected_version || '—', status: systemStatus.php_selected_ok ? 'ok' : 'bad', }, @@ -3790,6 +3849,29 @@ function Acp({ isAdmin }) {
CLI
+

+ CLI default php: {systemStatus?.php_default || '—'} ( + {systemStatus?.php_default_version || 'unknown'}){' '} + {cliDefaultPhpIsSufficient ? ( +