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.
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.
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.
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'; } foo(); $string = foo();
The warning can still be surpressed by using the new (void) cast:
(void) foo();
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.
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}
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);
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();
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'; }
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!