What's new in PHP 8.5

Written on 2025-11-20

PHP 8.5 was released on November 20, 2025. It includes 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,
        ]);
    }
}

I think this is a great feature. The only thing I find unfortunate is that it doesn't work when cloning readonly properties from the outside (which I think is a common use case). To do so, you have to specifically reset the propery's write access to public(set). I explained 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 warning 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 explicitly 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 built-in attributes (like #[Override]) are validated at compile-time rather than at runtime when being called via reflection. The #[DelayedTargetValidation] allows you to postpone that validation to a runtime:

class Child extends Base
{
	#[DelayedTargetValidation]
	#[Override]
	public const NAME = 'Child';

	// Note that this is an example, you cannot currently add #[Override] on constants
}

This attribute is added to manage backwards compatibility issues. You can read a concrete example here.


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…

it would be nice, if you tell people, why you have to put arrow functions in round brackets for pipe operator and what happens if you dont do this

Written by Sergey Poprygin on 2025-11-21

I'm pretty bummed about the backtick depreciation. Using backticks made it really easy to write little one-off shell scripts in PHP that were just a little too complex for bash. I talked to Crell about it on Mastodon and he said essentially "We're running out of keyboard characters and are trying to claw some back." I found the argument uncompelling.

Written by Jesse Donat on 2025-11-17
Noticed a tpyo? You can submit a PR to fix it.
HomeRSSNewsletterDiscord© 2025 stitcher.io Login