Home: Roshan Kc

← All posts

PHP · Releases 9 min read

PHP 8.5 Features That Actually Matter in Production Code

Direct answer: The PHP 8.5 features that matter most in production are the pipe operator (|>) for readable data transformations, clone() with property overrides for immutable objects, the built-in URI extension for standards-compliant URL parsing, #[\NoDiscard] for catching ignored return values, backtraces on fatal errors, and array_first() / array_last(). PHP 8.5 was released on November 20, 2025, and is supported until December 31, 2029.

Release notes list everything. This post covers the features that change everyday production code, with before/after examples and where each one fits. Our runtime at MarginTop runs on 8.5, so these are the ones I'd point a team to first. It's part of my PHP in 2026 series.

The pipe operator: |>#

The pipe operator passes the value on its left into the callable on its right, so transformations read top to bottom instead of inside out.

// Before: read from the innermost call outwards
$slug = strtolower(str_replace('.', '', str_replace(' ', '-', trim($title))));

// PHP 8.5
$slug = $title
    |> trim(...)
    |> (fn($s) => str_replace(' ', '-', $s))
    |> (fn($s) => str_replace('.', '', $s))
    |> strtolower(...);

Two practical notes:

  • Each step must be a callable that takes one argument. Arrow functions need parentheses around them.
  • The arrow-function wrappers are the awkward part. PHP 8.6's partial function application removes them: |> str_replace(' ', '-', ?).

Where it fits: input normalisation (phone numbers, slugs, imported CSV fields) and small data pipelines in jobs. Where it doesn't: anything with branching or error handling. A pipeline that needs if statements should stay a normal function.

clone() with property overrides#

Readonly classes made immutable value objects easy to define but awkward to modify. Every "wither" method had to rebuild the whole object. In 8.5, clone() takes an array of properties to change:

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public function withAmount(int $amount): self
    {
        return clone($this, ['amount' => $amount]);
    }
}

For DTOs and value objects (money, date ranges, API request objects) this removes a lot of constructor-forwarding code and the bugs that come with it. Forgetting to pass one argument through is the classic one.

The URI extension#

parse_url() has never followed any URL standard, and its edge cases have caused real security bugs such as SSRF filters bypassed by URLs that parse_url() and the HTTP client read differently. PHP 8.5 ships a URI extension that's always available, with two standards-compliant parsers:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://api.example.com:8443/v1/orders?page=2');
$uri->getHost();  // "api.example.com"
$uri->getPort();  // 8443

// Uri\WhatWg\Url parses the way browsers do

Use it for: anything security-sensitive, such as webhook URL validation, allowlists for outbound requests, and redirect targets. If you validate URLs with parse_url() and then fetch them with a different library, the gap between the two parsers is where attackers live.

#[\NoDiscard]#

Mark a function whose return value must be used, and PHP warns when a caller ignores it:

#[\NoDiscard("the result tells you whether the charge succeeded")]
function charge(Order $order): ChargeResult { /* ... */ }

charge($order);          // Warning: return value should be used
(void) charge($order);   // explicitly ignored, no warning

This is ideal for operations that report failure through the return value rather than an exception: payment results, lock acquisition, "was anything updated?" counts.

Backtraces on fatal errors#

Fatal errors such as "maximum execution time exceeded" now include a stack trace. Before 8.5, a timeout in production gave you a file and line number and nothing about how you got there. For debugging slow endpoints and stuck queue workers, this alone is worth the upgrade.

Security footnote: the fatal_error_backtraces setting behind this feature was part of the PHP2Shell attack chain on unpatched versions. Make sure you're on 8.5.12 or later.

array_first() and array_last()#

$latest = array_last($events);   // null if empty
$first  = array_first($items);

Small, but it replaces reset()/end() (which move the array's internal pointer), $arr[array_key_last($arr)], and a lot of helper functions.

Smaller changes worth knowing#

FeatureWhat it gives you
Closures in constant expressionsStatic closures and first-class callables in attribute arguments and defaults, useful for validation and access-control attributes
Persistent cURL share handlesDNS and connection reuse across requests, which means fewer TLS handshakes for API-heavy apps
#[\Override] on propertiesCatches renamed parent properties
Asymmetric visibility for static propertiesExtends the PHP 8.4 feature to static properties
final on promoted constructor propertiesLess boilerplate in class hierarchies
Closure::getCurrent()Recursive anonymous functions without use (&$fn) tricks
get_error_handler(), get_exception_handler()Inspect handlers without replacing them

Deprecations to fix before upgrading#

  • The backtick operator (`ls`, an alias for shell_exec()) is deprecated.
  • Non-canonical casts (boolean), (integer), (double) and (binary) are deprecated. Use (bool), (int), (float) and (string).
  • null as an array offset is deprecated.
  • Ending case statements with a semicolon is deprecated.
  • __sleep() / __wakeup() are soft-deprecated in favour of __serialize() / __unserialize().
  • New warnings when casting floats that don't fit into int, and when casting NAN.

Rector handles most of these automatically. The process is in the upgrade playbook.

Is upgrading to 8.5 worth it?#

If you're on 8.3 or 8.4: yes, and it's one of the easiest upgrades in years. The deprecations are small, and the debugging improvements alone pay for the effort. If you're on 8.2 or older, 8.5 is the right target, because it's supported until the end of 2029.

Key takeaways#

  • PHP 8.5 (November 2025) is supported until December 2029, making it the best target for upgrades today.
  • The pipe operator, clone() with overrides and array_first()/array_last() remove everyday boilerplate.
  • The URI extension and #[\NoDiscard] are quiet security and correctness wins.
  • Backtraces on fatal errors make production timeouts debuggable, but run 8.5.12+ because of PHP2Shell.

Sources#