Moving an application from PHP 8.4 to PHP 8.5 is usually a small step, but "usually" is not a plan. The official migration guide lists the backward-incompatible changes and deprecations; this guide picks out the ones that most often affect real applications and servers, shows how to find them in your code, and walks through switching a Debian or Ubuntu server without a long outage.
If you are coming from 8.3 or older, work through each intermediate migration guide (8.3 to 8.4, then 8.4 to 8.5) rather than jumping straight to the last one.
Breaking changes most likely to affect you
Configuration and engine
disable_classesis gone. The INI setting was removed because it broke engine assumptions. Configurations that still set it no longer get the protection they expected.- OPcache is part of the binary. It is always built in and loaded;
zend_extension=opcache.sonow emits a warning. Remove such lines from configuration you carry over from older versions.opcache.enableandopcache.enable_clistill work. - New warnings on casts. Casting a float (or a float-like string) that cannot be represented as an integer now warns, for both explicit and implicit int casts, and so does casting
NANto other types. Code that converts untrusted numbers may start logging warnings. - Destructuring non-arrays (other than
null) with[]orlist()now emits a warning. - Attributes on invalid targets: applying
#[\Attribute]to an abstract class, enum, interface or trait is now a compile-time error instead of failing later in reflection. class_alias()no longer acceptsarrayorcallableas alias names.
Extensions
- PDO: constructor arguments used with
PDO::FETCH_CLASSnow follow normalcall_user_func_array()semantics, so string keys act as named arguments. The numeric values ofPDO::FETCH_GROUP,FETCH_UNIQUE,FETCH_CLASSTYPE,FETCH_PROPS_LATEandFETCH_SERIALIZEchanged, which matters only if code stores or hard-codes the numbers instead of using the constants. - Sessions: writing session data where a
$_SESSIONkey contains a pipe character (|) now emits a warning instead of failing silently, andsession_start()validates its options array more strictly. - MySQLi: calling the
mysqliconstructor again on an already-constructed object now throws anError. - Intl requires ICU 57.1 or later, and mbstring uses Unicode 17.0 data.
- Standard: passing integer
0as the locale argument ofsetlocale()now throws aTypeError;ArrayObjectno longer accepts enums. - Many functions in Bzip2, POSIX, Sockets, SNMP, Tidy and others now throw
ValueErrorfor out-of-range or null-byte arguments that were previously tolerated.
Deprecations to clean up
Deprecations keep working in 8.5 but produce E_DEPRECATED messages. Fixing them now keeps logs readable and prepares for a future major version. The ones that appear most in older codebases:
- Backtick shell execution (use
shell_exec()explicitly, or better, avoid shelling out). (boolean),(integer),(double),(binary)casts.caselabels ending with;instead of:.nullas an array offset or as the key inarray_key_exists()(use an empty string).- Incrementing non-numeric strings with
++(usestr_increment()). __sleep()/__wakeup()(soft-deprecated; move to__serialize()/__unserialize()).- No-op cleanup calls:
curl_close(),curl_share_close(),finfo_close(),imagedestroy(),xml_parser_free(). ReflectionProperty::setAccessible()and the other reflectionsetAccessible()methods, which no longer have any effect. These are common in older test suites and libraries.- Aliases
mysqli_execute()andsocket_set_timeout(); passing values outside 0 to 255 tochr(); passing a string that is not a single byte toord(). - The
uri:PDO DSN scheme, deprecated over the risk of DSNs loaded from remote URIs. - Deriving
$_SERVER['argc']and$_SERVER['argv']from the query string in non-CLI SAPIs: setregister_argc_argv=0and read$_GETor$_SERVER['QUERY_STRING']instead.
How to find them in your code
Start with cheap searches, then let tools and tests do the rest. Run from the project root and exclude vendor/ for your own code:
grep -rnE '\((boolean|integer|double|binary)\)' --include='*.php' --exclude-dir=vendor .
grep -rnE '\b(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free|mysqli_execute|socket_set_timeout)\s*\(' --include='*.php' --exclude-dir=vendor .
grep -rn 'setAccessible(' --include='*.php' --exclude-dir=vendor .
grep -rnE 'disable_classes|opcache\.so' /etc/php/Then:
- Lint with the new binary.
php8.5 -lcompiles files without running them, and since PHP 8.3 it accepts several files at once. It catches syntax problems but not anything that only happens at runtime.find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n 200 php8.5 -l | grep -v '^No syntax errors' - Run the test suite on 8.5 with deprecations visible. In the staging pool or
php.ini:error_reporting = E_ALL,log_errors = On,display_errors = Off, then read the log. - Static analysis. Tools such as PHPStan can analyse code against a target version (its
phpVersionparameter); make sure the tool version you install understands PHP 8.5. - Dependencies.
composer prohibits php 8.5(aliaswhy-not) lists packages whose constraints block PHP 8.5. Update those first.
Switching a Debian or Ubuntu server
Install 8.5 next to the current version, test it, then move traffic. On Ubuntu 26.04 LTS, php8.5 is in the distribution archive. On Debian 13, which ships 8.4, PHP 8.5 currently requires a third-party repository such as packages.sury.org; read its official instructions and pin it before adding it.
sudo apt update
sudo apt install php8.5-fpm php8.5-cli php8.5-mysql php8.5-xml php8.5-mbstring php8.5-curl php8.5-intl
# compare loaded extensions with the old version
php8.4 -m > /tmp/mods-84.txt; php8.5 -m > /tmp/mods-85.txt; diff /tmp/mods-84.txt /tmp/mods-85.txtCarry over only your own overrides (pool settings in /etc/php/8.5/fpm/pool.d/ and small files in conf.d/), not a whole old php.ini. Then point the web server at the new socket:
# Nginx: in the PHP location block
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
sudo nginx -t && sudo systemctl reload nginx
# Apache with PHP-FPM
sudo a2disconf php8.4-fpm
sudo a2enconf php8.5-fpm
sudo apachectl configtest && sudo systemctl reload apache2Keep the 8.4 FPM service installed and running until you are confident. Rolling back is then just pointing the socket back and reloading. Remember that CLI cron jobs and queue workers use the php command, which Debian-style packages manage with update-alternatives; check php -v and set it explicitly with sudo update-alternatives --config php if needed.
Pitfalls
- Upgrading PHP without upgrading the framework or CMS first. Check the vendor's supported versions.
- Missing extensions after the switch, which typically shows up as class or function "not found" errors. The
diffabove catches them. - OPcache settings tuned for the old version and copied blindly. Start from the packaged defaults and re-apply only what you measured.
- Forgetting worker processes (queues, schedulers, supervisors) that keep running the old binary until they are restarted.
What you should do
- Read the official 8.4 to 8.5 migration guide once, end to end, and note the items that touch your code.
- Run the searches, lint and test suite against 8.5 in staging, with all deprecations logged.
- Clear blocking Composer constraints with
composer prohibits php 8.5. - Install 8.5 side by side, switch the FPM socket, and keep the old version ready for a quick rollback.
- After the switch, watch error logs for a few days, then remove the old PHP packages.
Sources
- php.net/manual/en/migration85.php
- php.net/manual/en/migration85.incompatible.php
- php.net/manual/en/migration85.deprecated.php
- php.net/manual/en/migration85.other-changes.php
- php.net/releases/8.5/en.php
- php.net/manual/en/features.commandline.options.php
- getcomposer.org/doc/03-cli.md#prohibits-why-not
- phpstan.org/config-reference#phpversion
- packages.ubuntu.com/resolute/php8.5-fpm
- packages.debian.org/trixie/php
- salsa.debian.org/php-team/php/-/blob/debian/main/8.4/debian/php-fpm.conf
- salsa.debian.org/php-team/php/-/blob/debian/main/8.4/debian/php-cli.postinst.extra
Comments
0 total · 0 threads