Development

Modern PHP 8: Features That Changed How We Build Web Apps

From named arguments and enums to property hooks and the pipe operator, PHP 8.x has transformed how modern web applications are written. Here is what each release added and how it changes everyday code.

10 min readHiddlins Editorial Team

If your mental image of PHP was formed by WordPress plugins from 2012, modern PHP will surprise you. Across six releases — 8.0 in 2020 through 8.5 in late 2025 — the language gained named arguments, enums, readonly classes, property hooks, asymmetric visibility and, most recently, a pipe operator. The result is code that is shorter, safer and much easier for static analysis tools to understand. This guide walks through the features that changed how we design web applications, release by release, and finishes with a practical approach to upgrading legacy code.

Why PHP 8 matters, and which version to run#

Two things make the PHP 8 series a turning point. First, the type system matured: union, intersection, DNF, never, standalone null/false/true and typed class constants allow you to describe almost any contract precisely. Second, the language removed ceremony: constructor property promotion, match, enums and property hooks eliminate whole categories of boilerplate.

Running a supported version also matters for security. Each PHP branch gets two years of active support followed by two years of security-only fixes. According to the official supported versions page, the status in October 2026 is:

Branch Released Active support until Security fixes until Status (Oct 2026)
8.0 Nov 2020 — Nov 2023 End of life
8.1 Nov 2021 — 31 Dec 2025 End of life
8.2 Dec 2022 31 Dec 2024 31 Dec 2026 Security only, ending soon
8.3 Nov 2023 31 Dec 2025 31 Dec 2027 Security only
8.4 Nov 2024 31 Dec 2026 31 Dec 2028 Active
8.5 Nov 2025 31 Dec 2027 31 Dec 2029 Active

If your site still runs on PHP 7.x, 8.0 or 8.1, it is running on software that no longer receives security patches from the PHP project. That is one of the first things we check in any WordPress security review.

PHP 8.0: the foundation#

PHP 8.0 was the biggest single jump in the language’s modern history. The features we use daily:

  • Constructor property promotion declares and assigns properties straight from the constructor signature.
  • Named arguments make calls self-documenting and let you skip optional parameters.
  • Union types such as int|string, plus the mixed and static return types.
  • The match expression: strict comparison, no fall-through, returns a value and throws UnhandledMatchError if nothing matches.
  • The nullsafe operator?-> short-circuits a chain when a value is null.
  • Attributes, native structured metadata replacing docblock annotations.
  • Quality-of-life additions: str_contains(), str_starts_with(), str_ends_with(), throw as an expression, WeakMap and saner string-to-number comparisons.
<?php
// PHP 7.4 style: 15 lines of declarations and assignments
// PHP 8.0 style:
final class Invoice
{
    public function __construct(
        public string $number,
        public int $amountCents,
        public ?Customer $customer = null,
    ) {}
}

$invoice = new Invoice(number: 'INV-1042', amountCents: 149_00);

$label = match (true) {
    $invoice->amountCents >= 1_000_00 => 'large',
    $invoice->amountCents >= 100_00   => 'medium',
    default                            => 'small',
};

$country = $invoice->customer?->address?->country ?? 'unknown';

PHP 8.0 also introduced a JIT compiler. It is impressive for CPU-bound work, but typical web requests are dominated by database and network I/O, so for most sites OPcache — not the JIT — remains the performance feature that matters.

PHP 8.1: enums, readonly and fibers#

PHP 8.1 delivered the feature many developers had wanted for years: native enumerations. Enums replace class constants and magic strings with a closed set of values the type system understands. Backed enums map cleanly to database columns and API payloads, and they can carry methods and implement interfaces.

<?php
enum OrderStatus: string
{
    case Pending   = 'pending';
    case Paid      = 'paid';
    case Shipped   = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending   => 'Awaiting payment',
            self::Paid      => 'Paid',
            self::Shipped   => 'On its way',
            self::Cancelled => 'Cancelled',
        };
    }

    public function canBeRefunded(): bool
    {
        return in_array($this, [self::Paid, self::Shipped], true);
    }
}

$status = OrderStatus::from($row['status']);      // throws on unknown value
$maybe  = OrderStatus::tryFrom($input) ?? OrderStatus::Pending;

Also in 8.1:

  • Readonly properties can be initialized once and never modified — ideal for value objects.
  • First-class callable syntax: strlen(...) or $this->handle(...) creates a closure without string-based callables.
  • new in initializers allows objects as default parameter values.
  • Pure intersection types (Countable&Iterator) and the never return type.
  • Fibers, a low-level primitive that async libraries use to implement non-blocking I/O without callback pyramids.
  • array_is_list() and final class constants.

PHP 8.2: readonly classes and stricter objects#

PHP 8.2 built on 8.1’s foundations. Readonly classes make every declared property readonly with one keyword, which makes immutable data transfer objects trivial to write. Disjunctive Normal Form (DNF) types combine unions and intersections, such as (Countable&Iterator)|null, and null, false and true became usable as standalone types.

<?php
final readonly class Money
{
    public function __construct(
        public int $amountCents,
        public string $currency,
    ) {}

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new self($this->amountCents + $other->amountCents, $this->currency);
    }
}

The change with the biggest real-world impact was the deprecation of dynamic properties. Assigning an undeclared property on an object now raises a deprecation notice unless the class opts in with #[\AllowDynamicProperties]. Older WordPress plugins relied on this heavily, and these notices are often the first thing that floods the logs after a PHP upgrade. PHP 8.2 also added the Random extension with a proper object-oriented API, constants in traits, and the #[\SensitiveParameter] attribute, which redacts values such as passwords from stack traces.

PHP 8.3: safer refactoring#

PHP 8.3 was a smaller release focused on correctness and refactoring safety:

  • Typed class constants: const string PREFIX = 'inv_'; prevents child classes from changing a constant’s type.
  • The #[\Override] attribute makes PHP throw an error if a method marked as an override does not actually override a parent or interface method — catching typos and silent breakage when a parent method is renamed.
  • Dynamic class constant fetch: Config::{$name}.
  • Readonly properties can be re-initialized inside __clone(), which enables proper deep cloning of immutable objects.
  • json_validate() checks JSON syntax without decoding it into memory, plus mb_str_pad() and new Randomizer methods.
<?php
final class StripeGateway implements PaymentGateway
{
    #[\Override]
    public function charge(Money $amount, string $token): PaymentResult
    {
        // If PaymentGateway::charge() is renamed, this now fails loudly
        // instead of silently becoming an unused method.
    }
}

PHP 8.4: property hooks and asymmetric visibility#

PHP 8.4 changed how classes are designed. Property hooks attach get and set logic directly to a property, so you no longer need a private field plus a getter and setter just to validate or normalize a value. A property whose hooks do not reference the stored value is “virtual” and computed on read. Asymmetric visibility lets a property be publicly readable but only privately writable. The property hooks manual page covers the full rules.

<?php
final class Customer
{
    // Normalized on every write
    public string $email {
        set (string $value) {
            $value = strtolower(trim($value));
            if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException('Invalid email');
            }
            $this->email = $value;
        }
    }

    // Virtual property: computed, no storage
    public string $displayName {
        get => trim("{$this->firstName} {$this->lastName}");
    }

    // Anyone can read, only this class can write
    public private(set) int $loyaltyPoints = 0;

    public function __construct(
        public string $firstName,
        public string $lastName,
        string $email,
    ) {
        $this->email = $email;
    }
}

Other notable 8.4 additions from the official release notes:

  • new without parentheses: new Request()->withHeader(...).
  • Array helpers: array_find(), array_find_key(), array_any() and array_all().
  • A standards-compliant HTML5 parser via Dom\HTMLDocument, a major improvement for scraping and content processing.
  • #[\Deprecated] for your own functions and constants, lazy objects for frameworks, BcMath\Number, mb_trim() and driver-specific PDO subclasses.
  • Implicitly nullable parameters are deprecated: write ?Type $x = null instead of Type $x = null.

PHP 8.5: the pipe operator and friends#

PHP 8.5, released on 20 November 2025, continues the trend toward expressive, functional-friendly code. Its release announcement highlights:

  • The pipe operator |>, which passes a value through a chain of callables left to right instead of nesting calls inside out.
  • Clone with: clone($this, ['alpha' => $alpha]) simplifies “wither” methods on readonly classes.
  • A built-in URI extension for parsing and modifying URLs according to RFC 3986 and the WHATWG URL standard.
  • #[\NoDiscard], which warns when a function’s important return value is ignored.
  • array_first() and array_last(), plus closures and first-class callables in constant expressions and backtraces for fatal errors.
<?php
$slug = $title
    |> trim(...)
    |> (fn (string $s) => preg_replace('/[^a-z0-9]+/i', '-', $s))
    |> strtolower(...)
    |> (fn (string $s) => trim($s, '-'));

How modern PHP changes application design#

Individually, these are syntax improvements. Together, they shift how we structure applications:

  • Value objects become the default. Readonly classes, promotion and clone-with make small immutable types like Money, EmailAddress or Sku cheap to write, which pushes validation to the edges of the system.
  • Enums replace stringly-typed state. Order statuses, user roles and payment methods become exhaustive, and match forces you to handle every case.
  • Static analysis gets dramatically better. Tools like PHPStan and Psalm can verify far more when types live in the code rather than in comments, and many bugs are caught before code ever runs.
  • Less framework magic is needed. Attributes give routing, validation and serialization a native home; property hooks remove the need for magic __get() and __set().

Attributes in practice#

Attributes deserve a closer look because they changed how frameworks and libraries are configured. Before PHP 8, metadata lived in docblock comments that had to be parsed as strings — a typo was silently ignored. Attributes are real syntax: they are validated by the parser, can be namespaced and autoloaded like classes, and are read through the Reflection API. Here is a minimal router that discovers routes from attributes:

<?php
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Route
{
    public function __construct(
        public string $path,
        public string $method = 'GET',
    ) {}
}

final class ProductController
{
    #[Route('/products/{sku}')]
    public function show(string $sku): Response { /* … */ }

    #[Route('/products', method: 'POST')]
    public function store(Request $request): Response { /* … */ }
}

// Collect routes once at boot (and cache the result in production)
foreach ((new ReflectionClass(ProductController::class))->getMethods() as $method) {
    foreach ($method->getAttributes(Route::class) as $attribute) {
        $route = $attribute->newInstance();
        $router->add($route->method, $route->path, [ProductController::class, $method->name]);
    }
}

The same mechanism powers validation rules, serialization hints, dependency injection configuration and the built-in engine attributes such as #[\Override], #[\Deprecated], #[\SensitiveParameter] and #[\NoDiscard]. Because attributes are code, your IDE can autocomplete them and refactoring tools can rename them safely.

Errors instead of silent failures#

A quieter but important shift runs through the whole 8.x series: many situations that used to produce warnings or return false now throw TypeError or ValueError. Internal functions validate their arguments consistently, comparisons between numbers and non-numeric strings behave sensibly, and match throws when no arm matches. Code becomes more predictable — and bugs that previously hid in the logs surface immediately during testing.

Tip

  • Adopt features where they reduce risk first: enums for state, readonly value objects for money and identifiers, #[\Override] on every overriding method.
  • Raise your static analysis level gradually; a baseline file lets you fix new code strictly while legacy code catches up.

Upgrading legacy code and WordPress sites#

Most upgrade pain comes from a handful of predictable changes: dynamic property deprecations, stricter type handling in internal functions, removed functions, and deprecations such as implicitly nullable parameters. A safe process looks like this:

  1. Inventory. List your PHP version, extensions, Composer dependencies, plugins and themes.
  2. Scan statically. PHPCompatibility rules for PHP_CodeSniffer and PHPStan flag many incompatibilities without running the code.
  3. Automate the mechanical work. Rector can apply many version upgrades automatically, such as adding nullable types or promoting constructor properties.
  4. Test on staging with error_reporting(E_ALL) and logging enabled; deprecation notices are your to-do list.
  5. Upgrade one minor version at a time when jumping several versions, so each failure has a clear cause.
  6. Switch production, keep logs under watch for a week, and make sure OPcache is enabled and sized appropriately.

For WordPress and WooCommerce, core itself runs on current PHP 8 releases; the risk sits in plugins and themes, especially abandoned ones. Upgrading PHP usually also improves server response times, which helps your Core Web Vitals and keeps busy stores responsive, as discussed in our WooCommerce performance guide. If your host makes it hard to choose a PHP version, that is a sign it may be time to revisit your hosting choice.

Key takeaways

  • Run PHP 8.4 or 8.5 for new work; anything below 8.2 no longer receives security fixes.
  • 8.0 and 8.1 brought the essentials: promotion, named arguments, match, enums and readonly properties.
  • 8.2 to 8.4 made immutability and encapsulation first-class: readonly classes, #[\Override], property hooks, asymmetric visibility.
  • 8.5 adds the pipe operator, clone-with and a native URI API.
  • Upgrade with static analysis, Rector and a staging environment — not by flipping the switch in production.

Conclusion: PHP has grown up#

Modern PHP is a typed, expressive language with a predictable release cycle and excellent tooling. Taking advantage of it means more than changing a version number: it means modelling your domain with enums and value objects, letting static analysis catch mistakes early, and keeping your runtime on a supported branch.

Whether you are planning a new platform or modernizing an ageing codebase, our Custom Web Application Development service builds on current PHP with these practices from day one. If you simply need your existing site kept on a supported, secure PHP version, our Website Care & Maintenance Plan covers it. Contact us to discuss your project.

Frequently asked questions

Which PHP version should I use in 2026?

For new projects, use PHP 8.4 or 8.5, both of which are in active support. PHP 8.2 receives only security fixes until the end of 2026 and PHP 8.1 and older no longer receive any updates, so sites on those versions should be upgraded.

Is it safe to upgrade a WordPress site to PHP 8.4?

WordPress core runs on current PHP 8 releases, but compatibility depends on your plugins and theme. Test on a staging copy with error logging enabled, check deprecation notices, update or replace incompatible extensions, and then switch production.

Does the PHP JIT make websites faster?

The JIT compiler mainly helps CPU-heavy workloads such as numerical computation or image processing. Typical web requests spend most of their time on database queries and I/O, so the JIT rarely makes a noticeable difference there, while OPcache remains essential.

What are property hooks in PHP 8.4?

Property hooks let you attach get and set logic directly to a class property, so you can validate or transform values and define computed properties without writing separate getter and setter methods. They work together with asymmetric visibility, such as public private(set).

Written by the Hiddlins Editorial Team

We are a full-stack web development studio. Every article is researched, tested and reviewed according to our editorial policy. Questions or corrections? Tell us.

Have a project in mind?

Tell us what you are building. We reply within one business day with honest advice, a realistic timeline and a fixed-price proposal.