Modern PHP is a strictly typed, fast and predictable language, and PHP 8.3 is where that shift became hard to ignore. It added typed class constants, the #\[\\Override\] attribute, a native json_validate() function and a stronger Random extension. PHP 8.4 and 8.5 then delivered property hooks, asymmetric visibility and the pipe operator. Together they make PHP a competitive choice for APIs, SaaS products and content platforms in 2026.
This guide covers what each release changed, shows how to use the features that matter most in real code, and gives you a step-by-step plan to upgrade an existing application safely.
Key takeaways
- PHP 8.3 adds typed class constants, #[\Override], json_validate() and a stronger Randomizer: small changes that stop whole classes of bugs.
- PHP 8.4 brings property hooks and asymmetric visibility, which remove most getter and setter boilerplate.
- PHP 8.5 adds the pipe operator, a standards-compliant URI extension and clone-with for immutable objects.
- PHP 8.2 leaves security support on 31 December 2026; start new projects on PHP 8.4 or 8.5.
- Upgrade with automation: CI on both versions, Rector for code changes, PHPStan for the target version, then a gradual rollout.
Where PHP stands in 2026
PHP releases a new minor version every November. Since the release policy changed in 2024, each version gets two years of active support (bug and security fixes) and then two years of security-only fixes, with every phase ending on 31 December.
VersionReleasedActive support untilSecurity fixes untilPHP 8.1November 2021Ended31 Dec 2025 (end of life)PHP 8.2December 2022Ended31 Dec 2026PHP 8.3November 202331 Dec 202531 Dec 2027PHP 8.4November 202431 Dec 202631 Dec 2028PHP 8.5November 202531 Dec 202731 Dec 2029
PHP 8.2 support ends soon
PHP 8.2 stops receiving security fixes on 31 December 2026, and PHP 8.1 already has none. If you run either in production, plan the upgrade now. Check php.net/supported-versions for the current dates.
For a new project, start on PHP 8.4 or 8.5. For an existing application, PHP 8.3 is the minimum sensible target today, and the steps later in this guide work for any jump between 8.x versions.
PHP 8.3: safer contracts and fewer surprises
PHP 8.3 has no single headline feature. Instead it closes small gaps that used to let bugs slip through. These are the changes worth adopting first.
Typed class constants
Class, interface and enum constants can now declare a type. Before 8.3, a child class could silently replace a string constant with an integer and nothing would complain until production. Now the engine enforces the type, including in implementations of an interface.
interface HasApiVersion
{
const string VERSION = 'v1';
}
final class BillingClient implements HasApiVersion
{
const string VERSION = 'v2'; // fine: still a string
// const int VERSION = 2; // fatal error: type must stay string
}
How to adopt it: add types to constants in interfaces and base classes first, because that is where accidental overrides happen. Rector can add them for you (see the upgrade steps below).
The #[\Override] attribute
Mark a method with #\[\\Override\] and PHP checks that a parent class or interface really has a method with that name. If someone later renames or removes the parent method, your code fails loudly at compile time instead of quietly never being called.
abstract class Controller
{
protected function authorize(): bool
{
return false;
}
}
final class ReportController extends Controller
{
#[\Override]
protected function authorize(): bool
{
return $this->user->can('view-reports');
}
}
This is especially useful in framework code, where you override hooks such as boot() or handle() and a typo would otherwise create a new, unused method.
json_validate()
json\_validate() checks whether a string is valid JSON without building the array or object. It uses less memory than json\_decode(), which matters when you only need a yes or no, for example when rejecting bad webhook payloads early.
$payload = file_get_contents('php://input');
if (!json_validate($payload)) {
http_response_code(400);
exit('Invalid JSON');
}
$event = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
If you are going to decode the data anyway, skip the extra check and call json\_decode() with JSON\_THROW\_ON\_ERROR; validating first only pays off when invalid input is common or the payload is large. To turn incoming JSON into typed objects, the free JSON to PHP class converter generates readonly DTOs from a sample payload.
A better Randomizer
The Random\\Randomizer class (introduced in 8.2) gained methods that replace common hand-written helpers. By default it uses a cryptographically secure engine, so it is safe for codes and tokens.
use Random\Randomizer;
$random = new Randomizer();
// An 8-character voucher code without look-alike characters (no 0/O, 1/I).
$code = $random->getBytesFromString('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', 8);
// A float in a range, e.g. a 5–15% discount for an experiment.
$discount = $random->getFloat(0.05, 0.15);
Smaller changes you will use
-
Dynamic class constant fetch:
Status::{$name}reads a constant or enum case by name, replacingconstant()calls. -
Readonly properties in **
\_\_clone()***:* readonly properties can be re-initialised while cloning, so deep copies of immutable objects finally work. -
Specific date exceptions: invalid dates throw exceptions such as
DateMalformedStringExceptioninstead of a genericException, so you can catch exactly what you expect. -
mb\_str\_pad(): padding that counts characters correctly in multibyte strings such as Tamil, Hindi or emoji. -
php -l** with several files:** lint a whole changed-file list in one call in CI.
Performance
Each 8.x release brings incremental gains in the Zend Engine (PHP’s core execution engine). They aren’t dramatic “2x faster” headlines, but they compound, especially for high-traffic applications. Most real applications gain more from correct OPcache settings than from any single release, so configure those first (see step 6 below).
PHP 8.4: property hooks and asymmetric visibility
PHP 8.4 (November 2024) is the biggest change to how classes are written since constructor promotion in PHP 8.0.
Property hooks
A property can define its own get and set logic. You keep the simple $user->email syntax while still validating or normalising values, so most getters and setters can go.
final class User
{
public string $email {
set(string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: {$value}");
}
$this->email = strtolower($value);
}
}
// A virtual property: computed on read, never stored.
public string $handle {
get => strstr($this->email, '@', true);
}
public function __construct(string $email)
{
$this->email = $email;
}
}
$user = new User('Ada@Example.com');
echo $user->email; // ada@example.com
echo $user->handle; // ada
Asymmetric visibility
Asymmetric visibility lets a property be public to read but private (or protected) to write. It is the cleanest way to expose state that only the object itself may change.
final class Invoice
{
public private(set) string $status = 'draft';
public function send(): void
{
// ... deliver the invoice ...
$this->status = 'sent';
}
}
$invoice = new Invoice();
echo $invoice->status; // draft
$invoice->status = 'paid'; // Error: cannot modify private(set) property
Other PHP 8.4 highlights
-
New array functions:
array\_find(),array\_find\_key(),array\_any()andarray\_all()replace manyforeachloops with a single readable call. -
No parentheses needed after **
new***:*new Cart($items)->total()now works. -
#\[\\Deprecated\]: mark your own functions and methods as deprecated so callers see the same notices as for built-in ones. - Lazy objects: built-in support for objects that initialise on first use, which frameworks use for faster dependency injection.
-
HTML5 parsing: the new
Dom\\HTMLDocumentparses modern HTML correctly, unlike the old libxml-basedDOMDocument.
$overdue = array_find($invoices, fn (Invoice $i) => $i->dueAt < new DateTimeImmutable());
$allPaid = array_all($invoices, fn (Invoice $i) => $i->status === 'paid');
One change can break older code: implicitly nullable parameters such as function find(int $id = null) are deprecated. Write ?int $id = null instead; Rector fixes this automatically.
PHP 8.5: the pipe operator and safer URLs
PHP 8.5 (November 2025) focuses on readability and safety.
The pipe operator
The pipe operator |> passes the value on its left into the callable on its right, so data flows top to bottom instead of inside out.
// Before: read from the inside out.
$slug = strtolower(str_replace(' ', '-', trim($title)));
// PHP 8.5: read top to bottom.
$slug = $title
|> trim(...)
|> (fn (string $s) => str_replace(' ', '-', $s))
|> strtolower(...);
Each step must take exactly one argument, and arrow functions need parentheses around them, as shown above.
A standards-compliant URI extension
parse\_url() doesn’t follow any URL standard exactly, and the differences have caused real security bugs such as allow-list bypasses. PHP 8.5 ships a URI extension with parsers for RFC 3986 and the WHATWG URL standard that browsers use.
use Uri\Rfc3986\Uri;
$uri = new Uri('https://phpscientist.com/blog/?page=2');
echo $uri->getHost(); // phpscientist.com
echo $uri->getQuery(); // page=2
More PHP 8.5 additions
-
Clone with:
clone($this, \['amount' => $amount\])copies an object and changes properties in one step, which makes “wither” methods on readonly classes trivial. -
array\_first()** and *array\_last():* get the first or last value of any array without touching its internal pointer. -
#\[\\NoDiscard\]: warns when a caller ignores a return value that matters, such as a result object or a lock status. - Backtraces for fatal errors: out-of-memory and timeout errors now show where they happened.
- OPcache always included: it is now part of every PHP build, so there is no excuse to run without it.
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
public function withAmount(int $amount): static
{
return clone($this, ['amount' => $amount]);
}
}
Why this matters: predictability over cleverness
Older PHP favoured flexibility. Modern PHP favours correctness. Strict types, enums (8.1), readonly classes (8.2), typed constants (8.3) and property hooks (8.4) all move errors from runtime to the moment code is written or analysed.
That matches how software engineering has matured overall. Teams now rely on static analysis, automated tests and strict contracts between components, and every type you add gives tools such as PHPStan and Psalm more to check. The language and its tooling now reinforce each other.
Rule of thumb
If a feature lets a mistake fail at compile time or in static analysis instead of in production, adopt it. That covers typed constants, #[\Override], readonly classes, enums and asymmetric visibility.
Frameworks have followed. Laravel and Symfony require recent PHP versions, use attributes for routing and validation, and encourage typed, testable code. If you are weighing PHP against other runtimes, the comparison in PHP vs Node.js in 2026 covers where each fits.
How to upgrade to PHP 8.3 or later safely
Upgrades fail when they are done by hand in one large step. This sequence keeps every step small, automated and reversible.
Step 1: Check what blocks the upgrade
Find out which dependencies don’t yet support your target version, and update them first.
php -v
composer why-not php 8.4 # packages that block PHP 8.4
composer outdated --direct # direct dependencies with newer releases
Step 2: Run your tests on both versions in CI
Add the target version to your CI matrix next to the current one, so every change is tested on both until you switch.
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.3', '8.4']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- run: composer install --no-progress
- run: vendor/bin/phpunit
Step 3: Let Rector apply the mechanical changes
Rector rewrites code automatically: it adds types, fixes deprecated syntax and adopts new features. Run it on a branch and review the diff like any other pull request.
// rector.php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withPhpSets(php83: true);
composer require --dev rector/rector
vendor/bin/rector process --dry-run # preview
vendor/bin/rector process # apply
Step 4: Tell static analysis the target version
Configure PHPStan for the version you are moving to, so it reports code that will break there.
# phpstan.neon
parameters:
level: 6
phpVersion: 80300
paths:
- src
Step 5: Hunt deprecations in staging
Today’s deprecation is the next version’s fatal error. Log E\_DEPRECATED in staging (not in production output) and fix what appears while real traffic exercises the code.
; staging php.ini
error_reporting = E_ALL
display_errors = Off
log_errors = On
Step 6: Tune OPcache and measure
Switch the server, then compare response times and memory before and after. These production settings are a good starting point:
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0 ; clear OPcache on each deploy
opcache.jit = tracing
opcache.jit_buffer_size = 64M
JIT helps CPU-heavy work such as image processing or calculations far more than typical database-bound web requests, so keep it only if your measurements show a gain. For the rest of a production php.ini, see basic PHP settings for secure, high-performance applications, or paste your file into the free php.ini security checker.
Step 7: Roll out gradually and keep a way back
Deploy the new version to one server, container or a small share of traffic first, watch error rates and logs, then roll out fully. Keep the previous runtime image available until the new version has run cleanly for a few days.
Common upgrade problems and fixes
-
“Implicitly marking parameter as nullable is deprecated” (8.4): change
Type $x = nullto?Type $x = null. Rector fixes all of them at once. -
Dynamic property deprecation (since 8.2): declare the property, or add
#\[\\AllowDynamicProperties\]to legacy classes you can’t change yet. -
A package refuses to install: check for a newer major version, or temporarily use
composer install --ignore-platform-req=phpin a test branch only to see what else breaks. -
Different date behaviour: catch the new specific date exceptions, and always set
date.timezoneexplicitly.
A version upgrade is also a good moment to review the rest of your stack against current security practices for modern PHP.
Final thought
Programming languages don’t just evolve technically; they evolve philosophically. PHP’s recent releases show a language growing up: less magic, more clarity, less “it works somehow” and more “it works because”.
The web still runs on PHP. What has changed is how it runs: faster, stricter and easier to reason about. If you are starting something new in 2026, PHP deserves a serious look, not out of nostalgia but out of pragmatism. If you are maintaining an older application, the path to PHP 8.4 or 8.5 is now mostly automated, so the best time to start is this sprint.
Originally published at phpscientist.com.
Top comments (0)