PHP 8.0 support
Exponential 6 runs on PHP 8.0 and later, and from 6.0.15 it is usable without support on PHP 7.4 (see
Why Exponential supports PHP 7.4). PHP 8.0 matters because it is the
stock PHP of Red Hat Enterprise Linux 9 (and of AlmaLinux 9 and Rocky Linux 9). Read this page if you run Exponential on RHEL 9
without a newer PHP, if you write kernel or extension code, or if you wonder why a function newer than 8.0 is
wrapped in function_exists().
In short
| Oldest PHP | 8.0 for the kernel and the shipped extensions, under Apache with mod_php, PHP-FPM or Velocity; 7.4 usable without support for the kernel from 6.0.15, without Velocity. |
| Velocity | Exponential Velocity, the application server, runs on PHP 8.0 too, from its release 0.0.4.54 (until then it needed 8.1; see below). |
| What changed in 6.0.15 | The few places that needed PHP 8.1 or later were made to work on 8.0; CI checks 8.0 on every push. |
| Behaviour on 8.1 and later | Unchanged. Every fallback is used only where PHP lacks the function. |
| Composer | composer.json declares ^8.0 and requires Velocity at ~0.0.4.57; a plain composer install works on 8.0 and installs the engine too. |
What was found and how it was fixed
The whole tree was checked with PHPCompatibility for PHP 8.0 (testVersion 8.0), every PHP file was linted with
PHP 8.0.30, every class of the kernel and of 63 extensions was loaded with PHP 8.0.30, and a complete installation
was served by PHP 8.0.30: the front page, 35 public pages, the administration interface logged in (dashboard,
content views, setup, notification and e-mail preference pages, the layout editor) and the editor siteaccess, with
no PHP error, warning or deprecation. The findings:
| Where | What | PHP | Fix |
|---|---|---|---|
kernel/classes/expvelocityconfiglayout.php | array_is_list() | 8.1 | defined in lib/phpcompat.php when missing |
kernel/classes/subitems/expsubitemscsvexport.php | fputcsv() with $eol (an ArgumentCountError on 8.0) | 8.1 | on 8.0 the line is written without $eol and its "\n" replaced by "\r\n" |
lib/eztemplate/classes/eztemplatestringoperator.php (wash), kernel/common/ezsimpletagsoperator.php and 26 other kernel, library, ezjscore, ezoe and expservices files | htmlspecialchars(), htmlentities(), html_entity_decode() and htmlspecialchars_decode() without $flags | 8.1 changed the default | the PHP 8.1 default ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 is passed explicitly, so single quotes are escaped on 8.0 too |
kernel/classes/contentjob/*, kernel/classes/ini/expinieditor.php | fsync() | 8.1 | already behind function_exists(); on 8.0 the data is flushed (fflush()) and left to the operating system |
kernel/classes/contentjob/expcontentjobworker.php | memory_reset_peak_usage() | 8.2 | already behind function_exists() |
lib/eztemplate/classes/eztemplatestringsoperator.php | str_increment(), str_decrement() | 8.3 | already behind function_exists(); the operators return their input on older PHP |
lib/ezdb/classes/ezdbquerycache.php | hash( 'xxh128' ) | 8.1 | already checks hash_algos(); md5 on 8.0 |
The htmlspecialchars() change is the one that matters for security: PHP 8.0's default leaves ' alone, so a
{$value|wash} inside a single-quoted HTML attribute would not have been safe on 8.0. Passing the flags gives
every PHP version the same output, the one 8.1 and later already produced. If you have compiled templates from
before the change, clear the template cache once (php bin/php/ezcache.php --clear-id=template).
Found on the way, and broken on every PHP 8, not only on 8.0 (fixed in the same change or in the extension's repository):
| Where | What | Fix |
|---|---|---|
kernel/private/modules/oauth/authorize.php | implode( $pieces, $glue ) (a TypeError since 8.0) | arguments swapped |
kernel/classes/datatypes/ezuser/ezldapuser.php | crypt() without a salt (an ArgumentCountError since 8.0) | a random password from random_bytes() |
syndication: eZSyndicateType::onPublish() | by-reference parameters against the by-value parent (a fatal error when the class loads) | & removed |
ezstarrating: ezsrRatingObjectTreeNode::makeObjectsArray() | lacks the parameters the kernel's method has (a fatal error when the class loads) | parameters added |
ezstarrating ezsrratingobject.php, swark SwarkJSONEncodeOperator, SwarkVariableNamesOperator | implode( $pieces, $glue ) | arguments swapped |
birthday ezbirthday.php | each(), removed in 8.0 | key() and current() |
| enhancedezbinaryfile | split(), removed in 7.0 | explode() |
Not changed, because they behave the same on 8.0 as on 8.5: optional parameters before required ones (a
deprecation since 8.0 in a few old signatures), the mysqli error mode (8.0 does not throw by default, and the driver
checks return values, which is what it expects), never as the return type of closures in xrowextract (8.0 reads it
as a class name; the closures always exit, so it is never checked).
How the compatibility functions work
lib/phpcompat.php defines a function only when PHP does not have it, with the same result as PHP's own:
if ( !function_exists( 'array_is_list' ) )
{
function array_is_list( array $array ) { ... }
}autoload.php reads it before anything else, and only on PHP older than 8.1 (PHP_VERSION_ID < 80100), so on 8.1
and later the file is not even opened and the built-in functions run. Every entry point (index.php, the CLI scripts,
cronjobs, the tests) starts with autoload.php, so they all have the functions.
Rules for new code:
- A function newer than PHP 8.0 that can be written in PHP with the same result goes into
lib/phpcompat.php. - One that cannot (
fsync(),memory_reset_peak_usage(),imageavif()...) is called behindfunction_exists(), with a fallback that is correct on 8.0. - No syntax newer than 8.0: enums,
readonly,never,newin initializers, first-class callable syntaxfoo(...), intersection types, typed class constants.#[\Override]and other attributes are harmless. - An argument a function gained later (
fputcsv()'s$eolin 8.1) is a fatal error on 8.0: branch onPHP_VERSION_ID. phpcs.xml.distruns PHPCompatibility withtestVersion8.0-, and the Quality workflow's jobphp80lints every file and boots the kernel on PHP 8.0 (quality checks).
Exponential Velocity on PHP 8.0
From release 0.0.4.54 (20 October 2026) Velocity (vendor/se7enxweb/exponential-velocity) runs on PHP 8.0 to 8.5:
its composer.json requires "php": ">=8.0" (it was >=8.1). Nothing extra is needed on 8.0, no polyfill package
and no extension. Until 0.0.4.54 the engine declared 8.1 as its minimum, so on PHP 8.0 a site had to be served by
Apache or PHP-FPM; that limit is gone. So:
- Apache or PHP-FPM serving the site: PHP 8.0 or later (PHP 7.4 usable without support).
- Velocity: PHP 8.0 or later, including the stock PHP 8.0 of RHEL 9.
What differs on PHP 8.0, and only this:
- AVIF image output needs PHP 8.1; on 8.0 the image negotiation stops at WebP.
- The dashboard's per-request memory peak needs PHP 8.2; on 8.0 the column is empty.
$_FILEShas nofull_path, as in PHP 8.0 itself.- A class declared with
eval()that has static properties makes its worker get replaced after that request; that is correct and costs one fork.
The engine handles the other differences itself: it reads declared static defaults from the source (PHP 8.0's
getDefaultValue() returns the current value), calls setAccessible() on 8.0 for private and protected statics, and
clears the stat cache for the dashboard's log tail. The zygote runs on 8.0. Extensions, systemd, SELinux and how to
verify an installation are in the engine's
PHP 8.0 and Red Hat Enterprise Linux
page; the engine's minimums are in its
requirements and the release in
its changelog.
With that, Velocity is part of Exponential as of 6.0.15: the root composer.json requires it at ~0.0.4.57, so
the kernel, the shipped extensions and the server share one minimum, PHP 8.0. The story for every reader:
Velocity is part of Exponential 6.0.15.
Red Hat Enterprise Linux 9
- Stock PHP 8.0:
dnf install php php-fpm php-mysqlnd php-gd php-intl php-mbstring php-xml php-pdo php-opcacheinstalls PHP 8.0 from AppStream. That is enough for Exponential under Apache, PHP-FPM or Velocity. The el9 rpm of Velocity (exponential-velocity-<release>-1.el9.noarch.rpm) depends onphp-cli >= 8.0and installs on it without enabling a module stream. - Newer PHP from AppStream modules: RHEL 9 also offers the module streams
php:8.1,php:8.2andphp:8.3(dnf module list php; switch withdnf module reset phpanddnf module enable php:8.3, thendnf distro-sync). Each of them runs Exponential and Velocity too; none is needed for Velocity. - RHEL 8: the default PHP there is 7.2. Enable the 8.0 stream first (
dnf module enable php:8.0) and install Velocity from the phar or with Composer; there is no el8 rpm. - Remi or Plesk: the Remi repository (
php:remi-8.xmodule streams) and Plesk's/opt/plesk/php/8.xbuilds provide every version from 8.0 to 8.5 side by side. - Extensions the stock packages leave out (
php-pecl-redis,php-pecl-apcu,oci8) come from EPEL, Remi or the vendor; Exponential uses them only when their features are configured.
Installing with Composer on PHP 8.0
composer.json declares "php": "^8.0 || ^8.1 || ..." since 6.0.15. The extensions that only declared ^8.1 in
their metadata were released with ^8.0 (explayouts and its packages, the expsite packages, exp_enhanced_link,
expchangeclass, expquery-translator, sevenx-themes-media 1.5.32, exponential-legacy-installer 2.2.4), and
syndication 1.3.5, ezstarrating 6.0.9, swark 1.0.5, birthday 1.3.3 and enhancedezbinaryfile 4.4.5 were released with fixes for code no PHP 8
could load.
Velocity is required again, at ~0.0.4.57 (0.0.4.54 was the first release that runs on PHP 8.0). Between its move to
"suggest" (because it needed PHP 8.1) and that release, it had to be added by hand; an installation made in that
time adds it with:
composer require se7enxweb/exponential-velocity:~0.0.4.57| Package | Declares | Blocks 8.0 |
|---|---|---|
google/recaptcha (via recaptcha) | 2.1.0 needs >=8.4 | no: the requirement is *, so Composer picks 1.3.x on 8.0 |
Every package composer.json requires allows PHP 8.0, so a plain composer install --no-dev works on RHEL 9's
stock PHP. The CI job php80 installs exactly that way, so a package that raises its minimum again fails there.