PHP 8.5, released on November 20, 2025, is a substantial release that introduces the pipe operator, clone-with syntax, a built-in URI extension, and several quality-of-life improvements. This guide covers what matters most when you are planning to upgrade a real production application.
Key PHP 8.5 changes
Pipe operator (|>)
The pipe operator chains callables left-to-right, replacing deeply nested function calls with a readable forward-flowing pipeline:
$slug = ' My New Blog Post! '
|> trim(...)
|> strtolower(...)
|> (fn($s) => str_replace(' ', '-', $s))
|> (fn($s) => preg_replace('/[^a-z0-9\-]/', '', $s));
// "my-new-blog-post"This is especially useful for data transformation chains in controllers, API response formatting, and ETL pipelines. See RFC: Pipe Operator v2.
Clone with
The clone() function now accepts a property override array, simplifying the “with-er” pattern for readonly classes:
readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): self
{
return clone($this, ['amount' => $amount]);
}
}
$price = new Money(1000, 'USD');
$discounted = $price->withAmount(800);This removes the boilerplate of manually creating new instances in value objects. See RFC: Clone with.
Built-in URI extension
PHP 8.5 ships a native Uri extension for parsing, normalizing, and constructing URLs following RFC 3986 and WHATWG URL standards:
use Uri\Rfc3986\Uri;
$uri = new Uri('https://example.com/api/v2/users?page=3');
var_dump($uri->getHost()); // "example.com"
var_dump($uri->getPath()); // "/api/v2/users"This replaces fragile parse_url() calls and third-party URL libraries for most use cases. See the Uri extension documentation.
#[\NoDiscard] attribute
Mark functions whose return values must not be silently ignored:
#[\NoDiscard("Result contains validation errors")]
function validate(array $data): ValidationResult
{
// ...
}
validate($input);
// Warning: The return value should either be used or intentionally ignored
$result = validate($input); // OK
(void) validate($input); // OK - explicitly ignoredThis catches a common bug pattern where developers call a function for side effects but forget to check its return value. See RFC: #[\NoDiscard] attribute.
array_first() and array_last()
Two frequently-needed helpers finally land in core:
$events = getRecentEvents();
$latest = array_first($events); // first element, or null if empty
$oldest = array_last($events); // last element, or null if emptyThese replace the reset() / end() workarounds and array_key_first() + index access patterns. See RFC: array_first() and array_last().
Closures in constant expressions
Static closures and first-class callables now work in constant expressions, including attribute parameters and property defaults:
class EventBus
{
public const DEFAULT_HANDLER = static fn($e) => logger()->info($e->name);
}Additional improvements
- Fatal errors now include backtraces - significantly easier post-mortem debugging
Closure::getCurrent()- enables recursion in anonymous functions without assigning to a variable first- Persistent cURL share handles via
curl_share_init_persistent()- avoids repeated DNS/connection setup across requests #[\Override]on properties and#[\Deprecated]on traits and constants - broader attribute coverageget_error_handler()/get_exception_handler()- inspect current handlers without replacing them- DOM additions:
Element::getElementsByClassName()andElement::insertAdjacentHTML()
Real-world impact
Cleaner data pipelines
The pipe operator is the headline feature for application code. Any chain of transformations - input sanitization, API response shaping, report generation - reads more naturally:
$report = $rawData
|> filterInvalidEntries(...)
|> groupByRegion(...)
|> calculateTotals(...)
|> formatAsCsv(...);Better value objects
clone with makes readonly value objects practical without excessive boilerplate. If you are building domain models, DTOs, or configuration objects, this pattern replaces the repetitive new self(...) constructors.
Safer APIs
#[\NoDiscard] lets library authors and team leads enforce that critical return values (validation results, error codes, resource handles) are not silently dropped. Apply it to any function where ignoring the result is almost certainly a bug.
Backward-compatibility considerations
Backtick operator deprecated
The backtick operator (`) as an alias for shell_exec() is now deprecated. If your codebase uses backticks for shell commands, replace them:
// Deprecated
$output = `ls -la`;
// Use instead
$output = shell_exec('ls -la');Non-canonical cast names deprecated
(boolean), (integer), (double), and (binary) casts now emit deprecation notices. Use (bool), (int), (float), and (string) respectively.
__sleep() / __wakeup() soft-deprecated
These magic methods are soft-deprecated in favor of __serialize() and __unserialize(). No runtime warning yet, but IDEs and static analyzers will flag them.
Other changes to watch
disable_classesINI setting removed - if you relied on this for security hardening, use alternative approaches- Case statements with semicolons (instead of colons) are deprecated
- Using
nullas an array offset is deprecated - Casting
NANto other types now emits a warning - Float-to-int casts with precision loss now emit a warning
Dependency compatibility checks
Before upgrading, verify that your dependencies support PHP 8.5.
1. Check platform requirements
composer check-platform-reqsLook for any packages that cap at <8.5 or ^8.4.
2. Run a dry-run update
composer update --dry-run --ignore-platform-req=phpThis reveals dependency conflicts without modifying your lock file.
3. Review key packages
Check explicit support in these commonly-used packages:
- PHPUnit: version 12.x supports PHP 8.5. Update from 11.x if needed.
- Symfony: Symfony 7.x components support PHP 8.5. Check individual component constraints.
- Laravel: Laravel 12.x officially supports PHP 8.5.
- Doctrine ORM: version 3.x supports PHP 8.5.
- PHPStan / Psalm: update to latest releases for proper 8.5 syntax analysis (pipe operator, clone-with).
- Rector: update to latest for automated 8.5 deprecation fixes.
4. Test with CI first
Add a PHP 8.5 job to your CI matrix before switching production:
# GitHub Actions example
jobs:
test:
strategy:
matrix:
php: ['8.4', '8.5']Upgrade checklist
Use this as a step-by-step plan for moving a production app from PHP 8.4 to 8.5:
- Read the full PHP 8.5 migration guide and changelog
- Run
composer check-platform-reqsand resolve any version ceiling issues - Run your test suite on PHP 8.5 in CI - fix any failures
- Replace backtick operator usage with
shell_exec()calls - Replace non-canonical casts (
(integer),(boolean), etc.) with short forms - Migrate
__sleep()/__wakeup()to__serialize()/__unserialize() - Audit any code using
nullas an array offset - Update PHPStan / Psalm to latest and run a full analysis
- Update Rector to latest and run
rector processto auto-fix deprecations - Deploy to a staging environment and run integration / smoke tests
- Monitor error logs for deprecation notices after deployment
- Once stable, start adopting new features (pipe operator, clone-with,
#[\NoDiscard]) in new code
Conclusion
PHP 8.5 is a feature-rich release that brings long-requested syntax improvements to production code. The pipe operator and clone-with syntax address real pain points in everyday development, while #[\NoDiscard] and the URI extension strengthen code safety. The upgrade path from 8.4 is manageable - most breaking changes involve replacing deprecated syntax patterns that static analysis tools can catch automatically.
For the full details, refer to the official PHP 8.5 release announcement and the PHP 8.5 migration guide.