PHP 8.5: Every New Feature Explained with Real Code Examples

The pipe operator alone will change how you write PHP forever. And that’s just the beginning.


PHP 8.5 landed on November 20, 2025 — and it’s one of the more developer-friendly releases the language has shipped in years. No architectural upheaval, no migration nightmare. Just a focused set of features that make PHP cleaner, safer, and more expressive to write every day.

The current stable release is PHP 8.5.4 (March 2026), and it will receive active support until December 31, 2027. If you’re on PHP 8.4, upgrading is straightforward and genuinely worth your time.

Here’s every significant feature, explained with real before-and-after examples.


Feature 1: The Pipe Operator (|>) — PHP’s Most Wanted Addition

If there’s one feature developers have been requesting for years, it’s the pipe operator. PHP 8.5 finally delivers it.

The |> operator enables chaining callables left-to-right, passing values smoothly through multiple functions without intermediary variables.

The old way — deeply nested, read inside-out:

$title = ' PHP 8.5 Released ';

$slug = strtolower(
    str_replace('.', '',
        str_replace(' ', '-',
            trim($title)
        )
    )
);

// string(15) "php-85-released"

PHP 8.5 with the pipe operator — read left to right, top to bottom:

$title = ' PHP 8.5 Released ';

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

// string(15) "php-85-released"

The value flows naturally through the chain. Each step receives the output of the previous one as its single argument.

More real-world examples:

// String sanitization pipeline
$clean = $userInput
    |> trim(...)
    |> strip_tags(...)
    |> htmlspecialchars(...);

// Data transformation
$result = $rawJson
    |> json_decode(...)
    |> (fn($data) => array_filter($data, fn($u) => $u['active']))
    |> array_values(...);

// Formatting a number
$formatted = 1234567.891
    |> (fn($n) => round($n, 2))
    |> number_format(...);

Things to know about the pipe operator:

  • Works with closures, arrow functions, first-class callables (trim(...)), array callables, and invokable objects
  • Left-associative — chains evaluate left to right
  • Wrap arrow function steps in parentheses: |> (fn($s) => ...)
  • By-reference callables are not allowed on the right-hand side

The pipe operator alone will transform how you write data transformation code in PHP.


Feature 2: Clone With — Immutable Objects Finally Have Ergonomic Syntax

If you’ve ever worked with immutable value objects or readonly classes, you know the pain of the “wither” pattern:

// Before PHP 8.5 — manual property copying
class Color {
    public function __construct(
        public readonly int $red,
        public readonly int $green,
        public readonly int $blue,
        public readonly int $alpha = 255,
    ) {}

    public function withAlpha(int $alpha): self {
        return new self(
            red: $this->red,
            green: $this->green,
            blue: $this->blue,
            alpha: $alpha,   // Only this changed
        );
    }
}

Every “with” method had to manually copy every property, even the ones that didn’t change. Add a new property and you had to update every wither method.

PHP 8.5 — clone with property overrides:

readonly class Color {
    public function __construct(
        public int $red,
        public int $green,
        public int $blue,
        public int $alpha = 255,
    ) {}

    public function withAlpha(int $alpha): self {
        return clone($this, ['alpha' => $alpha]);
        // All other properties copied automatically
    }
}

$blue = new Color(79, 91, 147);
$transparentBlue = $blue->withAlpha(128);
// $blue unchanged, $transparentBlue has alpha = 128

A fuller real-world example — an immutable Order object:

readonly class Order {
    public function __construct(
        public string $id,
        public string $status,
        public float $total,
        public \DateTimeImmutable $updatedAt,
    ) {}

    public function markAsShipped(): self {
        return clone($this, [
            'status'    => 'shipped',
            'updatedAt' => new \DateTimeImmutable(),
        ]);
    }

    public function applyDiscount(float $percent): self {
        return clone($this, [
            'total'     => $this->total * (1 - $percent / 100),
            'updatedAt' => new \DateTimeImmutable(),
        ]);
    }
}

$order = new Order('ORD-001', 'pending', 299.99, new \DateTimeImmutable());
$shipped = $order->markAsShipped();
$discounted = $order->applyDiscount(10);

The clone() syntax respects type checks, visibility, and property hooks — all the normal assignment rules apply. The only restriction that’s lifted is the write-once nature of readonly properties during cloning.


Feature 3: Built-In URI Extension — parse_url() Finally Has a Worthy Successor

parse_url() has been in PHP since version 4. It doesn’t follow any standard, has quirky PHP-isms, and handles malformed URLs poorly. PHP 8.5 ships a brand-new URI extension powered by uriparser (RFC 3986) and Lexbor (WHATWG URL standard).

Old approach — fragile and non-standard:

$components = parse_url('https://php.net/releases/8.5/en.php');
var_dump($components['host']); // string(7) "php.net"
// No validation, no standard compliance, inconsistent with malformed input

PHP 8.5 — RFC 3986 compliant:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://user:pass@php.net:443/releases/8.5/en.php?v=latest#top');

echo $uri->getScheme();    // https
echo $uri->getHost();      // php.net
echo $uri->getPort();      // 443
echo $uri->getPath();      // /releases/8.5/en.php
echo $uri->getQuery();     // v=latest
echo $uri->getFragment();  // top
echo $uri->getUserInfo();  // user:pass

// Immutable — modifications return new objects
$updated = $uri->withHost('laravel.com')->withPath('/docs');
echo $updated; // https://user:pass@laravel.com:443/docs?v=latest#top

WHATWG URL standard (for browser-compatible URL handling):

use Uri\WhatWg\Url;

$url = new Url('https://example.com:8080/path?query=string#fragment');

echo $url->host;      // example.com:8080
echo $url->hostname;  // example.com
echo $url->port;      // 8080
echo $url->pathname;  // /path
echo $url->search;    // ?query=string
echo $url->hash;      // #fragment

// Safe factory method — returns null instead of throwing on invalid URLs
$invalid = Url::parse('not a url'); // null, no exception

Use Uri\Rfc3986\Uri for strict server-side URL validation and Uri\WhatWg\Url for URLs that need to behave consistently with how browsers interpret them.


Feature 4: #[\NoDiscard] Attribute — Catch Ignored Return Values

How many times have you called a method, forgotten to use the return value, and spent an hour debugging why nothing changed? PHP 8.5 introduces the #[\NoDiscard] attribute to catch this at runtime.

#[\NoDiscard]
function getPhpVersion(): string {
    return 'PHP 8.5';
}

getPhpVersion();
// Warning: The return value of function getPhpVersion() should
// either be used or intentionally ignored by casting it as (void)

$version = getPhpVersion(); // Correct — return value used
(void) getPhpVersion();     // Also correct — intentionally discarded

This is especially valuable on immutable objects and fluent APIs:

class QueryBuilder {
    #[\NoDiscard]
    public function where(string $condition): self {
        // Returns a new QueryBuilder with the condition added
        return clone($this, ['conditions' => [...$this->conditions, $condition]]);
    }
}

$query = new QueryBuilder();
$query->where('age > 18');           // Warning! Return value ignored
$filtered = $query->where('age > 18'); // Correct

DateTimeImmutable methods in PHP 8.5 now also carry #[\NoDiscard], which means the common mistake of calling $date->modify('+1 day') without assigning the result will now emit a warning.


Feature 5: array_first() and array_last() — Long Overdue

This is the “finally!” feature of PHP 8.5. Getting the first or last value of an array has always been awkward:

// Old ways — all have edge cases or side effects
$first = reset($array) ?: null;       // Moves internal pointer, falsy issue
$first = $array[array_key_first($array)] ?? null; // Two operations, verbose
$last  = end($array) ?: null;         // Same problems as reset()

PHP 8.5 — clean, simple, null-safe:

$users = ['Alice', 'Bob', 'Charlie'];

$first = array_first($users); // 'Alice'
$last  = array_last($users);  // 'Charlie'

// Works with associative arrays
$config = ['host' => 'localhost', 'port' => 3306, 'name' => 'mydb'];
array_first($config); // 'localhost'
array_last($config);  // 'mydb'

// Empty arrays return null — no warnings
$empty = [];
var_dump(array_first($empty)); // null
var_dump(array_last($empty));  // null

No side effects, no internal pointer movement, no edge cases. Just the value you want.


Feature 6: Fatal Error Backtraces — Debugging Gets Much Easier

Before PHP 8.5, fatal errors showed the location of the error but no stack trace. You’d see:

Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/app.php on line 45

And then you’d have to figure out how you got to line 45.

PHP 8.5 includes the full stack trace:

Fatal error: Maximum execution time of 1 second exceeded in example.php on line 6

Stack trace:
#0 example.php(6): usleep(100000)
#1 example.php(7): recurse()
#2 example.php(7): recurse()
#3 example.php(7): recurse()
...
#11 {main}

This is a small change with a big quality-of-life impact, especially in production environments where you’re debugging from logs.


Feature 7: Closures in Constant Expressions

Previously, closures and first-class callables couldn’t be used in constant expressions — which meant attributes couldn’t contain them. PHP 8.5 lifts this restriction.

// Define reusable callables as class constants
class StringHelper {
    public const TRIM  = trim(...);
    public const UPPER = strtoupper(...);
    public const LOWER = strtolower(...);
}

// Use closures in attribute parameters
#[SkipDiscovery(static function (Container $container): bool {
    return ! $container->get(Application::class) instanceof ConsoleApplication;
})]
final class BlogPostEventHandlers {}

// Validation rules as constants
class Validator {
    public const RULES = [
        'email' => fn($v) => filter_var($v, FILTER_VALIDATE_EMAIL),
        'phone' => fn($v) => preg_match('/^\+?[0-9]{10,15}$/', $v),
    ];
}

Note that closures in constant expressions must always be explicitly marked as static and cannot access the outer scope via use.


Feature 8: get_error_handler() and get_exception_handler()

Two small but useful introspection functions — now you can retrieve the currently active error and exception handlers:

// Before PHP 8.5 — no way to inspect current handlers
set_error_handler(fn($code, $msg) => logError($code, $msg));

// PHP 8.5 — inspect what's currently registered
$handler = get_error_handler();
// Returns the callable, or null if using PHP's default handler

$exceptionHandler = get_exception_handler();
// Same for exception handlers

Useful for debugging middleware stacks, testing that handlers are correctly registered, and building diagnostic tools.


What’s Deprecated in PHP 8.5

Every release deprecates old features. Here’s what to clean up:

// Deprecated: backtick operator (alias for shell_exec)
`ls -la`;   // Deprecated — use shell_exec('ls -la') instead

// Deprecated: non-canonical cast names
(boolean) $value;  // Use (bool)
(integer) $value;  // Use (int)
(double)  $value;  // Use (float)

// Deprecated: null as array offset
$arr[null];                   // Deprecated
array_key_exists(null, $arr); // Deprecated

// Deprecated: semicolon to end case statements
switch ($x) {
    case 1;    // Deprecated — use colon
    case 1:    // Correct
}

// Soft-deprecated: __sleep() and __wakeup()
// Use __serialize() and __unserialize() instead

How to Upgrade

# Check your current version
php -v

# Ubuntu / Debian
sudo apt install php8.5

# macOS with Homebrew
brew install shivammathur/php/php@8.5

# Update composer.json if you have PHP version constraints
"require": {
    "php": "^8.5"
}

Run your test suite, address any deprecation warnings, and you’re done. The upgrade from 8.4 to 8.5 is genuinely smooth for most applications.


Final Thoughts

PHP 8.5 is the kind of release that rewards daily PHP developers the most. The pipe operator directly addresses one of the most common code-readability complaints in PHP. Clone with eliminates pages of boilerplate from immutable object codebases. The URI extension finally gives PHP a standards-compliant URL API. And array_first() / array_last() solve something so basic it’s almost embarrassing it took this long.

None of these are revolutionary. All of them will improve the code you write tomorrow. And that’s exactly what a well-executed PHP minor release should do.

If you’re on 8.4, upgrade. If you’re on anything older, this is the push you needed.


PHP keeps getting better, one release at a time.

Leave a Reply

Your email address will not be published. Required fields are marked *