PHP 8.5: Major Features

PHP 8.5 continues the ā€œexpress more in the languageā€ trajectory: a pipe operator for readable data pipelines, #[\NoDiscard] so return values are not silently ignored, and closures in constant expressions so attributes and defaults can reference real callables. At the platform level, an always-on URI extension, stricter filter failure modes, and substantial PDO / Opcache behavior changes mean you should budget real QA time—not just for new syntax, but for extensions and INI packaging.

Table of Contents


Pipe operator (|>)

The pipe operator passes the left-hand value into a single-argument callable on the right. It shines when you chain several transformations without nested temporary variables.

$result = 'Hello World' |> strlen(...);
// Same as: $result = strlen('Hello World');

Rules that bite: the RHS must be a callable that accepts exactly one parameter (no by-ref). Arrow functions must be parenthesized when used with |> to avoid parse ambiguity. See the functional operators manual page for chaining examples.

Closures & first-class callables in constant expressions

PHP 8.5 allows closures and first-class callables in constant expressions, including:

  • Attribute arguments
  • Default values for properties and parameters
  • Constants and class constants

This makes metadata and defaults more expressive when a static scalar is not enough—while still being resolved at compile/link time where the language allows.

#[\NoDiscard] and the (void) cast

#[\NoDiscard] marks functions/methods whose return value should not be ignored (similar intent to C++ [[nodiscard]]). If the caller drops the return value, PHP can emit diagnostics (and static analysis tools can follow the same signal).

(void) is an explicit ā€œI am intentionally discarding this valueā€ marker. It does not change runtime behavior by itself, but it can suppress warnings tied to #[\NoDiscard] and help IDEs understand intent.

#[\NoDiscard]
function loadConfig(): array { return []; }

(void) loadConfig(); // intentional discard

URI extension (RFC 3986 / WHATWG URL)

PHP 8.5 adds an always-enabled uri extension for parsing and handling URIs/URLs per RFC 3986 and the WHATWG URL model. Use it when you need spec-correct URL/URI behavior instead of ad hoc string slicing (especially for user-controlled input).

Notable library & extension updates

  • Filter: FILTER_THROW_ON_FAILURE — validation failures can throw instead of returning false (cannot combine with FILTER_NULL_ON_FAILURE).
  • Intl: e.g. IntlListFormatter, Locale::addLikelySubtags() / minimizeSubtags(), extra NumberFormatter currency-related style constants (ICU version requirements apply).
  • Session / cookies: session_set_cookie_params(), session_get_cookie_params(), session_start(), plus setcookie() / setrawcookie() — support partitioned cookies via a "partitioned" key (CHIPS-style deployment patterns).
  • Standard: mail() with sendmail transport returns real errors and warns if the sendmail process dies unexpectedly; getimagesize() gains HEIF/HEIC, SVG (with libxml), and optional dimension unit metadata.
  • DOM: e.g. Dom\Element::$outerHTML, $children on Dom\ParentNode implementations.
  • cURL: new curl_getinfo() keys and options (proxy/auth introspection, large upload sizes, follow-location modes, TLS signature algorithms, etc.—often libcurl-version-gated).

New functions (array_first, array_last, handlers, …)

Highlights:

  • Core: get_error_handler(), get_exception_handler(), Closure::getCurrent()
  • Standard: array_first(), array_last()
  • Reflection: e.g. more on ReflectionConstant, ReflectionProperty::getMangledName()
  • Opcache: opcache_is_script_cached_in_file_cache()
  • PostgreSQL / SQLite / DOM: various driver helpers (see new functions)

Practical recipes

Pipe a normalization pipeline

$slug = $raw
    |> trim(...)
    |> strtolower(...)
    |> preg_replace('/\s+/', '-', ...);

Filter with exceptions instead of false

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE);

First / last element without manual indexing

$first = array_first($list);
$last  = array_last($list);

Backward incompatible changes (migration notes)

PHP core (language & runtime)

  • class_alias(): cannot use "array" or "callable" as alias names.
  • Loose compare to bool for ā€œuncomparableā€ objects (enums, CurlHandle, …): behavior unified with (bool)$object.
  • gc_collect_cycles() return value no longer counts strings/resources collected indirectly through cycles.
  • Final subclasses: may use self / concrete class name where static substitution was restricted.
  • Tick handlers: run after shutdown functions, destructors, and output handler cleanup.
  • Traits: binding order relative to parent class changed (subtle; re-test trait-heavy code).
  • Compilation/linking errors: error handling order changed; user error handlers throwing may behave differently.
  • #[\Attribute] on abstract class, enum, interface, or trait: compile-time error unless validation is deferred with #[\DelayedTargetValidation] (see manual).
  • disable_classes INI: removed.
  • Destructuring non-array, non-null with []/list(): warning.
  • Int cast of out-of-range floats / numeric strings, and NAN casts: warnings.

Opcache

  • Opcache is always built into the PHP binary and always loaded; loading zend_extension=opcache.so / .dll warns. Build flags --enable-opcache / shared opcache.so artifacts are gone—adjust deployment docs and Docker images.

PDO (high impact)

  • PDO::FETCH_* numeric values changed for several flags (FETCH_GROUP, FETCH_UNIQUE, …)—re-test any code that stores numeric fetch modes in databases/configs.
  • PDO::FETCH_CLASS constructor args: now follow call_user_func_array semantics (string keys behave like named args; by-ref wrapping rules changed).
  • Fetching: FETCH_PROPS_LATE only with FETCH_CLASS; FETCH_INTO restrictions with fetchAll(), etc.—see migration85 incompatible.

DOM / SPL / misc

  • Cloning live node lists / named node maps (classic + Dom\*) now fails (previously produced broken objects).
  • ArrayObject: no longer accepts enums as elements.
  • mysqli: calling constructor on an already constructed object throws Error.
  • Many extensions throw ValueError where they previously used TypeError or silent failure (FileInfo nul bytes, LDAP options, sockets ports, SNMP host validation, etc.).

Deprecations (high-level checklist)

Deprecations in 8.5 span core syntax (non-canonical casts like (integer)), legacy operators (backticks), serialization habits (__sleep/__wakeup soft-deprecation in favor of __serialize/__unserialize), PDO surface ( uri: DSN scheme, driver constants/methods on the base PDO class), cURL curl_close / share close, resource-style finfo_close, imagedestroy, xml_parser_free, and more. Treat the official migration85 deprecated list as the source of truth and schedule cleanups before they become hard errors.

Other changes & operations (Opcache, INI, performance)

  • CLI: --ini=diff prints INI deltas vs built-in defaults; cli_set_process_title() fails if the title is too long (no silent truncate).
  • Core INI: e.g. fatal_error_backtraces, max_memory_limit, startup-only caps on runtime memory_limit adjustments.
  • Opcache INI: e.g. opcache.file_cache_read_only for read-only file caches; JIT hot loop default tuned; memory consumption changes reported correctly.
  • Performance: faster exception creation, better array callbacks, urlencode speedups, Reflection property access improvements, SIMD paths on ARM, optional TAILCALL VM with Clang ≄ 19, and more—see other changes.

Closing thoughts

PHP 8.5 is a mix of ergonomic language features (pipes, richer constant expressions, discard semantics) and platform-level tightening (PDO modes, Opcache packaging, stricter validation across extensions). Plan upgrades as: adopt new syntax where it clarifies intent, then run integration tests against PDO fetch modes, session/cookie code paths, and anything that relied on deprecated resource-style close helpers or loose boolean comparisons to objects.