Skip to content

fix(cli): check the PHP extensions and functions ee needs before a command runs - #1957

Open
mrrobot47 wants to merge 3 commits into
EasyEngine:developfrom
mrrobot47:fix/php-requirements-check
Open

mrrobot47 wants to merge 3 commits into
EasyEngine:developfrom
mrrobot47:fix/php-requirements-check

Conversation

@mrrobot47

@mrrobot47 mrrobot47 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Fixes the core side of #1956: ee now checks the PHP extensions and functions it needs before a command runs, and says what to fix instead of dying with a PHP fatal error.

Problem

On PHP 8 a function listed in disable_functions is removed, so calling it is a fatal Call to undefined function. HestiaCP writes pcntl_* into the CLI php.ini, which made every ee site command fatal (#1956). The installer now refuses such a PHP (EasyEngine/installer#49), but nothing protected a host whose PHP changes later: a panel upgrade that rewrites php.ini, an extension removed by an apt upgrade or a PHP version switch, a Homebrew or manual phar install, or ee cli update.

What changes

  • New EE\Requirements (php/EE/Requirements.php) lists what ee calls without a guard and checks it with function_exists() / extension_loaded() only, so the check can't hit a disabled function itself. The happy path is about 30 function_exists() calls (a few microseconds); ini files are read only when something is missing.
  • Runner::check_requirements() runs it first, so it covers every command that already ran the Docker checks (everything except cli, config, help). A missing requirement is an Error: (exit 1) that names:
    • the disabled functions and the ini files whose disable_functions lists them, how to fix it (remove them for the PHP CLI), and, when it would be read last, a conf.d override that survives tools rewriting php.ini, e.g. /etc/php/8.3/cli/conf.d/99-easyengine.ini with the rest of the list;
    • missing extensions, with the Debian/Ubuntu package (apt-get install php8.3-curl php8.3-zip).
  • Optional functions only log a debug line: pcntl_signal (no rollback of an interrupted site command), pcntl_signal_dispatch, shell_exec (ee site share).
  • ee cli info shows a PHP requirements row (and the full message as a warning) and skips the migration until the requirements are met.
  • ee cli update only requires what it needs to run the new phar (proc_open, proc_close, stream_select, stream_set_blocking) and warns about the rest, so a host can still update to a fixed release. Downloads already fall back from curl to fsockopen.
  • ee cli self-uninstall only requires those and pdo_sqlite, and works before any migration created the sites table.
  • Two calls that ran before any check are gone: init-ee.php uses PHP_OS instead of php_uname(), and the first-run mkdir no longer uses shell_exec().
  • A failed requirement (including Docker or docker-compose missing) now exits 1 instead of 0 (die became EE::halt( 1 )).

Required vs optional

Required (refuses every command up front) = called without a guard on paths most commands or a main feature hit, so a command would otherwise fail part way:

Extension Functions / classes Used by
standard proc_open, proc_close, stream_select, stream_set_blocking every EE::exec() / EE::launch()
standard exec, escapeshellarg EE_DOCKER::container_status(), every shell command
standard chown, chgrp, symlink site create (site root, volume symlinks), update --php, v4.13.3 container migrations
standard disk_free_space, getmypid, php_uname, gethostname, parse_ini_string update migration, backup/restore, self-signed SSL, clone, dash, migrations
curl curl_init, curl_setopt, curl_exec, curl_getinfo, curl_error, curl_errno, curl_close, curl_version port and site checks, LE checks, backup callbacks
openssl openssl_x509_parse, openssl_x509_check_private_key, openssl_pkey_get_private Let's Encrypt, self-signed and custom SSL
posix posix_geteuid, posix_getegid, posix_getpwuid site and global service compose files
zlib gzopen, gzgets, gzwrite, gzeof, gzclose the log secrets migration
pdo_sqlite, zip PDO, ZipArchive the EE database, update --php, admin-tools
mbstring or iconv table output (the mbstring polyfill needs iconv)

Optional: pcntl_signal and pcntl_signal_dispatch (site-command degrades gracefully, EasyEngine/site-command#523), shell_exec (only ee site share; passthru and the other shell_exec uses are on the refused remote/vagrant paths). Already guarded: posix_isatty. Not checked: json (Monolog needs it while booting, before any check can run; built into PHP 8) and basic file functions.

On PHP 7 a disabled function only warns and returns null, so ee used to limp on with misleading errors (with exec/proc_open disabled it said Config root: /opt/easyengine is not writable). The check applies the same rule there, which means a PHP 7 host with a required function disabled is now refused with the real reason.

Merge order

Merge and tag EasyEngine/site-command#523 first. This PR treats pcntl as optional, so on its own (with the current site-command) a HestiaCP host would still hit the site-command fatal; the release that ships this should pin the fixed site-command.

Tests

  • Throwaway ubuntu:24.04 containers with ondrej PHP 8.3 and 7.4 and stubbed Docker, the released 4.13.3 phar vs a phar built from this branch and the site-command fix, with: HestiaCP's own CLI disable_functions line (v-add-web-php:145), Hestia's FPM list applied to the CLI (adds exec, proc_open, …), php_uname disabled, posix disabled (phpdismod), curl and zip disabled, and the suggested conf.d override:
    • 8.3 Hestia CLI list: before ee site list is a fatal in class-ee-site.php:87; after it runs (No sites found!), with PHP requirements: OK (optional, unavailable: pcntl_signal) in ee cli info.
    • Hestia FPM list: before every command (even cli info) is a fatal on shell_exec() in Runner.php:603 (8.3) or Config root ... is not writable (7.4); after cli info works and other commands get Error: EasyEngine needs the PHP functions exec, proc_open, which PHP 8.3.x (/usr/bin/php8.3) disables with disable_functions in /etc/php/8.3/cli/php.ini. plus the fix.
    • php_uname disabled: before a fatal in init-ee.php:7 on every command; after cli info works and other commands get the clean error.
    • posix or curl/zip missing: before site create died in HTML.php:227 (posix) or would fail later; after a clean error with apt-get install php8.3-common / php8.3-curl php8.3-zip.
    • ee cli update --stable with curl/zip missing or php_uname disabled warns and updates; with proc_open disabled it refuses cleanly. ee cli self-uninstall works with curl/zip missing and refuses cleanly without proc_open.
  • Unit tests (the local PHPUnit layer, not in this repo yet; PHP 7.4 and 8.5, child PHPs with their own ini files): classification, the ini file and override in the message (including a later conf.d file, inline comments and -d), unknown names, and that check_requirements() refuses before any Docker command.
  • Behat (fork CI, green): the core suite and the site-command, site-type-php and site-type-wp suites on PHP 7.4–8.5, plus the same suites with HestiaCP's disable_functions on 7.4, 8.3 and 8.5, and a job checking the clean errors with Hestia's FPM list, without curl and with php_uname disabled.

…mmand runs

On PHP 8 a function listed in disable_functions is removed, so calling one is a fatal "Call to undefined function". A host whose CLI php.ini disables functions ee calls (HestiaCP disables pcntl_*; other panels disable exec, proc_open, chown, symlink, curl_exec, ...) or whose PHP lacks an extension got a PHP fatal in the middle of a command instead of an error saying what to fix (EasyEngine#1956).

EE\Requirements lists the extensions and functions ee calls without a guard and checks them with function_exists()/extension_loaded() only, so the check itself can't hit a disabled function. Runner::check_requirements() runs it first, for every command it already covers: a missing requirement is an error naming the extensions or functions, the ini files whose disable_functions lists them, and a fix (remove them for the PHP CLI, with a conf.d override when one would be read last; the Debian/Ubuntu package for a missing extension). Optional functions (pcntl_signal, pcntl_signal_dispatch, shell_exec) only log a debug line.

ee cli info shows a "PHP requirements" row and the error as a warning, and skips the migration until the requirements are met. ee cli update only requires what it needs to run the new phar and warns about the rest, so a host can still update to a fixed release. ee cli self-uninstall only requires the process functions and pdo_sqlite, and works before any migration created the sites table.

Two boot-time calls that ran before any check are gone: init-ee.php uses PHP_OS instead of php_uname(), and the first-run mkdir no longer shells out. A failed requirement now exits 1 instead of 0.
…as PHP does

PHP splits disable_functions on spaces as well as commas, so a space-separated list was read as one name: the functions were reported as missing from the build instead of disabled, and the ini file and override were not named. When no ini file is scanned, the override hint now follows PHP_INI_SCAN_DIR (empty: no hint) instead of the compiled-in directory.
…lable

Requests selects its curl transport when curl_init() and curl_exec() exist, then calls curl_setopt() and curl_version() unchecked, so a PHP that disables one of those died with a fatal error in ee cli update (release lookup and downloads) instead of downloading.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The disable_functions parsers mishandle whitespace-separated entries, and the scan-directory fallback ignores PHP_INI_SCAN_DIR overrides, producing misleading configuration guidance.

2 open issues
What changed in this PR

Adds PHP requirement checks to EasyEngine’s command startup, replacing fatal errors with actionable diagnostics for disabled functions and missing extensions.

Changes:

  • Introduces required and optional PHP capability checks with configuration and installation guidance.
  • Adds requirement reporting to ee cli info and narrower checks for update and self-uninstall.
  • Removes early calls to potentially disabled functions and makes requirement failures exit with status 1.
File Description
php/​init-ee.php Uses PHP_OS instead of calling php_uname() during startup.
php/​EE/​Runner.php Checks PHP before Docker, returns failure status, and uses native directory creation.
php/​EE/​Requirements.php Defines PHP requirements and generates diagnostics and remediation hints.
php/​commands/​src/​CLI_Command.php Reports requirements and adjusts update and uninstall checks and handling.

Review effort: Balanced

Comment thread php/EE/Requirements.php Resolved
Comment thread php/EE/Requirements.php Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The configuration hint overlooks command-line overrides, and the recovery update path can still fail fatally when curl_setopt is disabled instead of falling back to Fsockopen.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Clarify CLI overrides can keep exec disabled

php/​EE/​Requirements.php:314

An ini file listing the function does not rule out a command-line override. With disable_functions = exec in php.ini and PHP launched with -d disable_functions=exec, this hint recommends 99-easyengine.ini, but applying that override still leaves exec disabled because -d wins. Qualify the hint and explain that the function must also be removed from any command-line disable_functions option.

Medium severity Fall back to Fsockopen when curl_setopt is disabled

php/​commands/​src/​CLI_Command.php:313

If curl is loaded but curl_setopt is disabled, ee cli update --stable now passes this reduced check and only warns, then dies during the download. Requests' Curl transport tests only curl_init and curl_exec before selecting itself, but its constructor calls curl_setopt unconditionally; it does not fall back to Fsockopen for this case. Select Fsockopen whenever a function needed by the Curl transport is unavailable, including for the release lookup and both downloads, so the recovery update path does not produce another PHP fatal.

2 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The override hint can recommend an ineffective ini change when -d disable_functions=exec takes precedence, so it must clarify that users need to change the command-line setting.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Explain that CLI disable_functions overrides php.ini settings

php/​EE/​Requirements.php:314

If php.ini lists exec and PHP is also started with -d disable_functions=exec, $files is nonempty and this recommends an override that cannot fix the problem. Applying the suggested disable_functions = "" override still leaves exec disabled because -d takes precedence over every ini file. Qualify the hint and tell users to change the command-line setting in this case.

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants