PHP 8.5 is not a revolution. It is a steady step forward that introduces a handful of new features, promotes a batch of deprecations to errors, and tightens type safety in ways that catch legacy codebases off guard. If your application was written for PHP 7.x or early 8.x, this checklist covers what you need to fix, test, and watch for during the upgrade.
The Working with Legacy PHP Codebases article covers the broader strategy for inheriting and improving old PHP projects. This guide focuses specifically on the PHP 8.5 version boundary.
Pre-Upgrade Preparation
Run Your Test Suite on the Current Version
Before touching PHP, confirm your tests pass on your current runtime. If they do not, fix them first. You cannot distinguish upgrade breakage from pre-existing breakage if your baseline is already broken.
Set Up a PHP 8.5 Test Environment
Use Docker to run tests against PHP 8.5 without changing your development machine:
1 | docker run --rm -v $(pwd):/app -w /app php:8.5-cli php -v |
Alternatively, use phpenv, asdf, or brew to install PHP 8.5 alongside your current version.
Run Deprecation Detection First
Enable full error reporting and run your application:
1 | php -d error_reporting=E_ALL -d display_errors=On public/index.php |
In your test suite:
1 | <!-- phpunit.xml --> |
Capture every deprecation notice. These are your migration task list.
New Features in PHP 8.5
The URI Extension
PHP 8.5 introduces a built-in Uri class for parsing and constructing URIs according to RFC 3986. This is a standard library addition, not a replacement for existing functionality, but it can conflict with userland code.
What to check in legacy code:
- If you have a class named
Uriin the global namespace, rename it. The standard library class takes precedence. - If your autoloader has special handling for a
Uriclass, verify it still resolves correctly. - Namespaced
Uriclasses (likeApp\Http\UriorLaminas\Uri\Uri) are unaffected.
Opportunity:
If your application does URL parsing with parse_url() and manual reassembly, consider adopting the new Uri class for cleaner code:
1 | $uri = Uri::parse('https://example.com/path?query=value'); |
The Pipe Operator
PHP 8.5 introduces the pipe operator (|>) for function chaining:
1 | $result = $input |
This is syntactic sugar. It does not break existing code, and you do not need to adopt it immediately. However, if your application includes custom code parsers, template engines, or tokenizers that process PHP source, they need to handle the new T_PIPE token.
Closures from Callable
PHP 8.5 continues the trend of improving closure creation. The Closure::fromCallable() syntax and (...) first-class callable syntax both work. Legacy code using call_user_func() and call_user_func_array() still works but receives no performance benefits from the runtime optimisations that closures get.
Deprecations Promoted to Errors
This is where most legacy code breaks. PHP 8.5 promotes several deprecations from PHP 8.3 and 8.4 to actual errors.
Dynamic Properties
Dynamic properties on classes that do not use #[AllowDynamicProperties] were deprecated in PHP 8.2 and continue to be enforced more strictly. In PHP 8.5, dynamic property access on non-stdClass objects emits E_WARNING or ErrorException depending on your error handler.
Fix:
Declare properties explicitly or add the #[AllowDynamicProperties] attribute:
1 | // Before (dynamic property) |
For ZF1/Laminas applications, framework classes typically handle this. Your own classes need auditing.
Implicit Nullable Type Declarations
The pattern function foo(Type $param = null) was deprecated in PHP 8.4. In PHP 8.5, this emits a deprecation notice that will become an error in PHP 9.0.
Fix:
Use explicit nullable syntax:
1 | // Before |
Find all instances:
1 | grep -Prn 'function\s+\w+\s*\([^)]*\w+\s+\$\w+\s*=\s*null' src/ app/ |
Serializable Interface
The Serializable interface is deprecated. Classes should implement __serialize() and __unserialize() instead:
1 | // Before |
Return Type Warnings on Built-in Interfaces
Classes implementing Iterator, Countable, ArrayAccess, JsonSerializable, or Stringable must declare return types that match the interface:
1 | // Before |
Extension Changes
ext-intl Updates
The Intl extension has updated ICU data and may produce different results for locale-sensitive operations. If your application formats dates, currencies, or numbers for specific locales, verify the output has not changed.
ext-openssl
OpenSSL functions continue to tighten their defaults. Weak cipher suites and short key lengths that produced warnings in PHP 8.4 may now fail. Check your TLS configuration if your application makes outbound HTTPS requests using stream_socket_client() or curl.
ext-pdo
PDO may change default error modes or type coercion behaviour for certain database drivers. Test your database queries thoroughly, especially around boolean columns, integer casting, and NULL handling.
Composer and Dependency Compatibility
Check All Dependencies
1 | composer why-not php 8.5 |
This command shows which packages in your dependency tree are not compatible with PHP 8.5 according to their composer.json constraints. Each result is a package you need to update or replace.
Update Incrementally
Do not run composer update and hope for the best. Update packages one category at a time:
- Framework packages first (laminas/, symfony/, laravel/*)
- Testing tools (phpunit, phpstan, psalm)
- Application-level libraries
- Development-only tools
After each batch, run your test suite.
Handle Platform Overrides
If a package has not updated its composer.json to allow PHP 8.5 but actually works, you can temporarily override the platform requirement:
1 | composer update --ignore-platform-req=php |
Use this sparingly and only after confirming the package works on PHP 8.5 through testing. Do not ship this override to production without validation.
Testing Strategy
Unit Tests
Run your full unit test suite on PHP 8.5. Most failures will be:
- Type errors from stricter type enforcement
- Return type mismatches on interface implementations
- Dynamic property access issues
- Deprecated function usage
Integration Tests
Integration tests catch issues that unit tests miss:
- Database driver behaviour changes
- Session handling differences
- File upload processing changes
- CURL and HTTP client behaviour
Smoke Tests
After fixing unit and integration test failures, deploy to a staging environment on PHP 8.5 and run through your critical user paths manually:
- Authentication (login, logout, session persistence)
- Form submissions (with validation errors and success paths)
- File uploads
- Payment processing
- Report generation
- Cron jobs and background tasks
Production Deployment
Use a Canary Deployment
If your infrastructure supports it, route a small percentage of traffic to a PHP 8.5 instance while the rest stays on the current version. Monitor error rates, response times, and memory usage. Increase the percentage gradually over several days.
Monitor Closely for 48 Hours
Some code paths only execute under specific conditions:
- Monthly billing runs
- Seasonal features
- Edge-case input combinations
- Low-frequency cron jobs
Watch your error log for the first 48 hours after cutover. Have a rollback plan ready.
Performance Baseline
PHP 8.5 includes JIT improvements and internal optimisations. Measure your application’s performance before and after the upgrade using the same workload. The PHP Performance Playbook covers the measurement methodology. Most applications see a modest performance improvement, but changed internal behaviour can occasionally affect specific patterns.
FAQ
Can I skip PHP 8.4 and go straight from 8.3 to 8.5?
Yes, but you will need to address all deprecations and breaking changes from both 8.4 and 8.5 simultaneously. It is more work in one step but reduces the total number of deployments.
How long should the upgrade take?
For a legacy application with 50,000 to 200,000 lines of code, expect one to three weeks of developer time for the fixes, testing, and deployment. Larger or older codebases take longer.
What about PHP extensions I rely on?
Check PECL for PHP 8.5-compatible versions of each extension. Extensions that use internal APIs that changed in PHP 8.5 may lag behind. Redis, Memcached, and Imagick typically update quickly. Niche extensions may take months.
Checklist Summary
- Test suite passes on current PHP version
- PHP 8.5 test environment set up
-
composer why-not php 8.5reviewed - Dependencies updated for PHP 8.5 compatibility
- Dynamic properties fixed or annotated
- Implicit nullable types made explicit
- Return types added to interface implementations
- Serializable interface replaced with __serialize/__unserialize
- Database queries tested on PHP 8.5
- Session handling verified
- Extension compatibility confirmed
- Full test suite passes on PHP 8.5
- Staging environment tested
- Performance baseline compared
- Production deployment with monitoring
Next Steps
Work through the checklist from top to bottom. Fix the test suite first, then address deprecations, then test integrations. The Modernising Zend Framework Applications guide covers the broader context of keeping legacy PHP applications current, and the Performance Optimisation chapter covers the server-level configuration that complements a PHP version upgrade.