Skip to content

Add PHP 8.5 support - #8

Merged
MarcelloDuarte merged 4 commits into
mainfrom
add-support-for-php-8.5
Aug 3, 2026
Merged

Add PHP 8.5 support#8
MarcelloDuarte merged 4 commits into
mainfrom
add-support-for-php-8.5

Conversation

@MarcelloDuarte

Copy link
Copy Markdown
Contributor

Why

Console did not declare PHP 8.5, and was in fact broken on it: Behat scored 228 of 421 scenarios. Every failure traced to one upstream deprecation — ImmMap::plus() backs every REPL session variable and called SplObjectStorage::attach(), deprecated in 8.5. Behat turns deprecations into exceptions, so every variable assignment threw.

Fixed upstream in phunkie 1.3.0 and pinned here. Behat on 8.5 now passes 484 of 484.

New syntax

The REPL interprets the AST rather than eval'ing it, so new syntax needs an explicit evaluator branch.

Casts had no branch at all — even (int) "42" reported Unsupported expression type. Adding only (void) would have left every sibling cast broken, so this adds one Expr\Cast branch covering int/float/string/bool/array/object, with (void) as one further arm reusing the existing __no_output__ path.

Pipe |> applies its right operand to its left rather than combining two values, so it gets its own handler ahead of the operator table, in the same shape as the existing Coalesce case. Semantics are native PHP 8.5 ($x |> f(...) is f($x)); composition sugar belongs in phunkiec, not here.

First-class callables for user-defined functions were rejected, documented as a permanent limitation because functions are "stored as AST nodes, not as callable PHP functions". They are stored as closures — the restriction was unnecessary. Piping into your own functions depends on it.

Deprecations are now reported, not hidden

8.5 deprecates a great deal, and the REPL handled deprecations three different ways depending on which path evaluated the code. The boundary swallowed them, while each of five eval'd definition sites installed its own handler that trapped every severity and turned it into a Failure — so a deprecated trait reported an Error even though the class was defined fine.

Deciding what a deprecation means is the boundary's job. The definition sites now share trapDefinitionErrors(), which keeps genuine errors local but passes deprecations out to the handler it replaced:

phunkie > $p->setAccessible(true)
Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5
$var0: Null = null

This also drops the @ from those eval() calls: it zeroed error_reporting for the whole call, which is what hid the deprecations, and bought nothing — the shared handler already returns true, so nothing leaks, and eval parse failures are ParseError throwables. The @ operator itself is unchanged.

Version gating

bin/run-behat-tests.sh grew one hardcoded elif per release, and its final else was labelled "8.4+", so features/repl/php8.5/ would have wrongly run on 8.4. It now derives exclusions from the directory names, so php8.6/ will need no change.

That also fixed a latent bug: the old 8.2/8.3 branches rooted their find at features/repl, silently never running features/execution. Scenario count on 8.2 goes 327 → 329, exactly the two scenarios in run_app.feature.

Coverage added

features/repl/php8.5/ — 51 scenarios across pipe operator, void cast, clone() with properties, array_first/array_last, error-handler introspection, final property promotion, #[\Override] on properties, the URI extension, and deprecation reporting. Plus features/repl/casts.feature (unversioned — casts are not an 8.5 feature).

Two expectations were corrected against the real runtime rather than the docs: replacing a readonly property via clone() is refused from global scope and only works inside the declaring class, and WhatWg host normalisation lowercases but does not decode percent-escapes.

Not done — blocked on gaps that predate 8.5

8.5 feature Blocked by
Asymmetric visibility for static properties Expr\StaticPropertyFetch unsupported
Missing trait as a catchable Error Stmt\TryCatch unsupported
Closures / FCC / casts in const expressions, attributes on global constants Stmt\Const_ unsupported
Closure::getCurrent() REPL interprets closure bodies, so there is no real closure

Each is its own pre-existing gap rather than 8.5 work; try/catch is probably the most valuable.

Verification

Behat, PHPUnit, PHPStan and PHP-CS-Fixer green on 8.2, 8.3, 8.4 and 8.5 — 341, 384, 433 and 484 scenarios respectively.

The REPL interprets the AST rather than eval'ing it, so new syntax needs an
explicit evaluator branch. Adds the two 8.5 expression forms, plus the gap one
of them sat on.

Casts had no evaluator branch at all: even (int) "42" reported "Unsupported
expression type". Adding only (void) would have left every sibling cast broken,
so this adds one Expr\Cast branch covering int, float, string, bool, array and
object, with (void) as one further arm. (void) discards its operand's value and
produces no output, reusing the existing __no_output__ path.

The pipe operator applies its right operand to its left rather than combining
two values, so it gets its own handler ahead of the operator table, in the same
shape as the existing Coalesce special case.

Piping into your own functions is the main use case, which ran into a documented
limitation: first-class callable syntax was rejected for user-defined functions,
on the stated grounds that they are "stored as AST nodes, not as callable PHP
functions". They are in fact stored as closures, so the restriction was
unnecessary. Removed it and replaced the "known limitation" scenario with the
behaviour that actually works.

Derives the Behat version gating from the feature directory names instead of a
hardcoded elif chain, so features/repl/php8.6/ will need no change here. This
also fixes a latent bug: the old 8.2 and 8.3 branches rooted their find at
features/repl, silently never running features/execution. Scenario count on 8.2
goes 327 -> 329, exactly the two scenarios in run_app.feature.

Bumps phunkie to 1.3.0, which fixes the SplObjectStorage deprecation that broke
every session variable assignment on 8.5. Behat there went from 228 of 421
passing to all of them.

Behat, PHPUnit, PHPStan and PHP-CS-Fixer green on 8.2, 8.3, 8.4 and 8.5:
341, 384, 433 and 447 scenarios respectively.
Class-level syntax already works because class definitions are re-emitted
through the pretty printer and eval'd, so the engine enforces the semantics.
New standard library calls pass straight through to PHP. Both only needed
acceptance coverage, added here as features/repl/php8.5/.

Covers clone($object, $withProperties), array_first/array_last,
get_error_handler/get_exception_handler, final constructor property promotion,
#[\Override] on properties, and the URI extension.

Two expectations were corrected against the real 8.5 runtime rather than the
documentation: replacing a readonly property via clone() is refused from global
scope and only works from inside the declaring class, and WhatWg host
normalisation lowercases but does not decode percent-escapes. Both are now
asserted as they actually behave.

479 scenarios green on 8.5, 341 on 8.2.
PHP 8.5 deprecates a great deal, and the REPL handled deprecations three
different ways depending on which path evaluated the code.

withEvaluationBoundary swallowed them, so $p->setAccessible(true) printed only
"Null = null" with no hint that it is deprecated. Meanwhile each of the five
eval'd definition sites installed its own error handler that trapped every
severity and turned it into a Failure, so a deprecated trait reported
"Error: Trait Legacy used by Consumer is deprecated" even though the class had
been defined perfectly well. Whether a deprecation was silent or fatal came down
to which handler happened to be installed.

Deciding what a deprecation means is the boundary's job, so the definition sites
no longer decide it. They share trapDefinitionErrors(), which keeps genuine
errors local but passes deprecations out to the handler it replaced. The
boundary collects them and attaches them to the result, and the REPL prints them
as a yellow advisory line above the value:

    phunkie > $p->setAccessible(true)
    Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5
    $var0: Null = null

Drops the @ from the eval() calls in those definition sites. It suppressed
error_reporting for the whole call, which is what hid the deprecations, and it
was not buying anything: the shared handler already returns true, so nothing
leaks to output, and eval() parse failures are ParseError throwables rather than
diagnostics @ could suppress.

Deprecations are de-duplicated per evaluation, so a loop does not print the same
notice repeatedly. The @ operator still suppresses everything, unchanged.

484 scenarios green on 8.5, and 341, 384 and 433 on 8.2, 8.3 and 8.4.
The 8.5 CI job failed while the same suite passed locally. GitHub Actions runs
with a production php.ini, whose error_reporting masks E_DEPRECATED, and the
evaluation boundary honoured that: the advisory line simply never appeared.

The REPL already decides how diagnostics are presented, so it should decide which
ones it sees. withEvaluationBoundary now raises error_reporting to E_ALL for the
evaluation and restores it afterwards, alongside the error handler it already
saves and restores. The @ operator is unaffected, since it masks reporting from
inside the expression being evaluated.

Doing this in the boundary rather than at startup matters: Behat resets
error_reporting around every single step, so anything configured when the REPL
starts is undone before the first expression is evaluated. The boundary is the
only place that covers the real REPL and the in-process test harness alike.

Renames installFatalErrorFormatter() to installDiagnosticsRendering(), which is
what it does now that it also silences PHP's own output, with the idempotent
part split into configureDiagnostics().

Verified by reproducing CI locally: the full 8.5 suite passes with
-d error_reporting="E_ALL & ~E_DEPRECATED" as well as with the default ini.
@MarcelloDuarte
MarcelloDuarte merged commit 20b8a01 into main Aug 3, 2026
12 checks passed
@MarcelloDuarte
MarcelloDuarte deleted the add-support-for-php-8.5 branch August 3, 2026 08:10
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.

1 participant