What's new in PHP 8.5

Written on 2025-11-14

PHP 8.5 will be released on November 20, 2025. It'll include the pipe operator, clone with, a new URI parser, and more.


The pipe operator

PHP 8.5 introduces the new pipe operator that makes chaining output from one function to another a lot easier. Instead of deeply nested function calls like this:

$input = ' Some kind of string. ';

$output = strtolower(
    str_replace(['.', '/', '…'], '',
        str_replace(' ', '-',
            trim($input)
        )
    )
);

You can now write this:

$output = $input 
    |> trim(...)
    |> fn (string $string) => str_replace(' ', '-', $string)
    |> fn (string $string) => str_replace(['.', '/', '…'], '', $string)
    |> strtolower(...);

I've done a deep-dive into this new operator, and you can read about it here.


Clone with

There's now a way to assign new values to cloned objects while cloning them:

final class Book
{
    public function __construct(
        public string $title,
        public string $description,
    ) {}
    
    public function withTitle(string $title): self
    {
        return clone($this, [
            'title' => $title,
        ]);
    }
}

While definitely a great feature, it's unfortunate that it doesn't work with cloning readonly properties (which I believe to be a very common use case). I wrote about the problem here.


(void) cast and #[NoDiscard]

You can now mark a function with the #[NoDiscard] attribute, indicating that its return value must be used. If nothing happens with that return value, a warning will be triggered.

#[NoDiscard("you must use this return value, it's very important.")]
function foo(): string {
    return 'hi';
}

// Warning:
// The return value of function foo() is expected to be consumed,
// you must use this return value, it's very important.
foo();

// This is ok:
$string = foo();

The exception can still be surpressed by using the new (void) cast:

(void) foo();

Closure improvements

Closures and first-class callables can now be used in constant expressions. In practice this means you'll be able to define closures in attributes, which is an incredible new feature:

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

Note that these kinds of closures must always be excplitly marked as static, since they aren't attached to a $this scope. They also cannot access variables from the outside scope with use.


Backtraces for fatal errors

A small but awesome change: fatal errors will now include backtraces.

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()
#4 example.php(7): recurse()
#5 example.php(7): recurse()
#6 example.php(7): recurse()
#7 example.php(7): recurse()
#8 example.php(7): recurse()
#9 example.php(7): recurse()
#10 example.php(10): recurse()
#11 {main}

Added array_first() and array_last()

Perhaps a bit overdue (array_key_first() and array_key_last() were added in PHP 7.3), but we finally get built-in functions to get the first and last elements from arrays! So instead of writing this:

$first = $array[array_key_first($array)] ?? null;

You can now write this:

$first = array_first($array);

URI parsing

There's a brand new URI implemention that makes working with URIs a lot easier:

use Uri\Rfc3986\Uri;

$uri = new Uri('https://tempestphp.com/2.x/getting-started/introduction');

$uri->getHost();
$uri->getScheme();
$uri->getPort();

// …

The #[DelayedTargetValidation] attribute

Some attributes like #[Override] or #[NoDiscard] can cause compile-time errors when used incorrectly. For example, when adding #[NoDiscard] to a function that returns void:

// Fatal error: A void function does not return a value, but #[NoDiscard] requires a return value
#[NoDiscard]
function foo(): void {}

Adding the new #[DelayedTargetValidation] to this function will supress the compile-time error, and will only trigger it if the attribute is being retrieved via reflection:

#[DelayedTargetValidation]
#[NoDiscard]
function foo(): void {}

Smaller changes


Deprecations and breaking changes


Those are the features and changes that stand out for PHP 8.5; you can find the whole list of everything that's changed over here.

What are your thoughts about PHP 8.5? You can leave them in the comments below!

Things I wish I knew when I started programming

Things I wish I knew when I started programming cover image

This is my newest book aimed at programmers of any skill level. This book isn't about patterns, principles, or best practices; there's actually barely any code in it. It's about the many things I've learned along the way being a professional programmer, and about the many, many mistakes I made along that way as well. It's what I wish someone would have told me years ago, and I hope it might inspire you.

Read more

Comments

Loading…
No comments yet, be the first!
Noticed a tpyo? You can submit a PR to fix it.
HomeRSSNewsletterDiscord© 2025 stitcher.io