Skip to main content
← All posts

Part 1: PHP 8.4 Architecture: Property Hooks, Asymmetric Visibility & Modern OOP

Md. Harun-Or-RoshidMar 10, 202610 min readArticle
Part 1: PHP 8.4 Architecture: Property Hooks, Asymmetric Visibility & Modern OOP

Series Overview & Learning Objectives

Welcome to Mastering Modern PHP 8.4 & Laravel 13: Enterprise Backend Architecture, Phase 1 of our comprehensive Modern Backend Engineering & Distributed Systems Roadmap. Modern PHP has evolved into a strictly-typed, high-performance, and elegant server runtime powering hundreds of millions of web applications and mission-critical enterprise systems. PHP 8.4 marks a watershed moment in the language's object-oriented ergonomics, introducing native Property Hooks, Asymmetric Visibility, typed class constants, new WHATWG HTML5 DOM parsers, standard array primitives, and enhanced JIT compiler optimizations. In this masterclass, we dissect how to leverage PHP 8.4 to eliminate boilerplate getters and setters, enforce immutability without sacrificing syntax ergonomics, construct clean domain entity boundaries, and optimize server-side memory execution.

1. The Evolution of PHP Object-Oriented Architecture

For over two decades, PHP developers building domain models adhered to standard JavaBean-style encapsulation patterns. To protect an object's internal invariant state, class properties were declared as private or protected, requiring developers to write verbose pairs of public getter and setter methods. In large domain-driven design (DDD) entities, an entity with 15 fields routinely generated over 120 lines of repetitive, boilerplate accessors.

While PHP 8.0 introduced Constructor Property Promotion and PHP 8.1 brought readonly classes, certain critical design tensions persisted. A readonly property could only be written once during instantiation and could never be modified or derived dynamically. Furthermore, if a property required validation (such as ensuring an email address format or clamping an integer within boundaries), developers were still forced to revert to legacy private properties with custom setter methods.

Architecture DimensionLegacy PHP (7.4 - 8.2)PHP 8.4 Modern Architecture
State EncapsulationPrivate properties with verbose getName() and setName() methods.Direct property access powered by native Property Hooks (get and set).
Read/Write ScopeUniform visibility across both read and write operations.Asymmetric Visibility: e.g., public private(set) string $id.
Virtual / Computed PropertiesExplicit methods like getFullName() requiring method call syntax.Virtual Property Hooks that compute values on-the-fly via property syntax.
Memory FootprintHigher call stack overhead from constant wrapper method invocation.Engine-level opcode optimization with zero additional call frame allocation.

2. Deep Dive: PHP 8.4 Property Hooks

Property Hooks introduce custom behavior directly inside property declarations. Hooks intercept reads via get and writes via set. Unlike magic methods like __get() and __set(), Property Hooks are strictly typed, analyzed statically by tools like PHPStan/Larastan, and executed at native C engine speed without reflection overhead.

Backed vs. Virtual Property Hooks

PHP 8.4 distinguishes between backed properties (properties that allocate dedicated storage in memory) and virtual properties (properties that exist only as computed expressions without allocating independent storage):

php
1234567891011121314151617181920212223242526272829303132333435363738394041424344
<?php

declare(strict_types=1);

namespace App\Domain\User;

use InvalidArgumentException;

class UserAccount
{
    // 1. Backed Property with Validation in the 'set' Hook
    public string $email {
        set {
            if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("Invalid user email format: {$value}");
            }
            // Assign to the underlying backing storage using $this->email
            $this->email = strtolower(trim($value));
        }
    }

    // 2. Backed Property with Transformation in the 'get' Hook
    public string $displayName {
        get => ucfirst($this->displayName);
        set => trim($value);
    }

    // 3. Virtual Property (No backing storage allocated in memory!)
    public string $gravatarHash {
        get => hash('sha256', strtolower(trim($this->email)));
    }

    public function __construct(string $email, string $displayName)
    {
        $this->email = $email;             // Executes 'set' hook validation!
        $this->displayName = $displayName; // Executes 'set' hook transformation!
    }
}

// Client Usage:
$user = new UserAccount('[email protected]', 'parvez');
echo $user->email;        // "[email protected]"
echo $user->displayName;  // "Parvez"
echo $user->gravatarHash; // SHA-256 hash computed on-the-fly!

3. Asymmetric Visibility: Elegant Immutability

In clean architecture and Domain-Driven Design (DDD), exposing entity attributes for reading is completely safe, but permitting arbitrary external mutation violates invariants. Previously, to create an ID or creation timestamp that anyone could inspect but only the entity itself could alter, developers were forced to create private properties with dedicated getter methods.

PHP 8.4 solves this with Asymmetric Visibility. You can now define distinct access levels for reading and writing:

php
12345678910111213141516171819202122232425262728293031323334353637
<?php

declare(strict_types=1);

namespace App\Domain\Billing;

use DateTimeImmutable;

class Invoice
{
    // Public for reading outside the class, but strictly private for mutation!
    public private(set) string $invoiceNumber;
    public private(set) int $amountInCents;
    public private(set) string $currency;
    public private(set) DateTimeImmutable $issuedAt;
    public protected(set) bool $isSettled = false;

    public function __construct(string $invoiceNumber, int $amountInCents, string $currency)
    {
        $this->invoiceNumber = $invoiceNumber;
        $this->amountInCents = $amountInCents;
        $this->currency = $currency;
        $this->issuedAt = new DateTimeImmutable();
    }

    public function settle(): void
    {
        $this->isSettled = true; // Permitted: internal mutation
    }
}

// Outside Client Code:
$invoice = new Invoice('INV-2026-001', 4900, 'USD');
echo $invoice->invoiceNumber; // "INV-2026-001" (Reads cleanly without getInvoiceNumber()!)

// $invoice->amountInCents = 1000;
// ERROR: Cannot modify private(set) property App\Domain\Billing\Invoice::$amountInCents from global scope!

4. Architectural Benefits: Eliminating Call Overhead & Boilerplate

The combination of Property Hooks and Asymmetric Visibility delivers profound architectural advantages for enterprise backend engineering:

  • Reduced Cognitive Load: Domain entity files shrink by 60% to 75% in total lines of code. Code reviews focus on core business logic rather than verifying mechanical getters and setters.
  • Opcode Optimization: In Zend Engine opcache, property hooks avoid creating standard PHP userland call frames when short expression hooks (get => ...) are compiled, cutting CPU cycle consumption in high-volume microservices.
  • Interface Compliance: PHP 8.4 allows interfaces to define property requirements with read/write contracts: public string $title { get; } or public int $status { get; set; }, unlocking uniform data contracts across polymorphic services.

5. New WHATWG Compliant HTML5 DOM Parser in PHP 8.4

For years, PHP's built-in \DOMDocument was notorious for adhering to the outdated libxml2 HTML4 parsing standard. Parsing modern HTML5 tags like <main>, <section>, or <article> generated countless warnings and frequently corrupted DOM document structures.

PHP 8.4 introduces a brand new, spec-compliant \Dom\HTMLDocument parser implementing the modern WHATWG HTML5 specification. This enables high-performance serverside web scraping, HTML sanitization, and RSS/AMP feed generation with zero third-party dependencies:

php
12345678910111213
<?php

declare(strict_types=1);

// Modern WHATWG spec-compliant HTML5 parsing in PHP 8.4:
$html = '<!DOCTYPE html><html><body><main class="content"><article><h1>PHP 8.4</h1></article></main></body></html>';

$doc = \Dom\HTMLDocument::createFromString($html, LIBXML_NOERROR);
$headings = $doc->querySelectorAll('main > article > h1');

foreach ($headings as $h1) {
    echo $h1->textContent . PHP_EOL; // "PHP 8.4" cleanly parsed!
}

6. Method Chaining on Instantiation Without Parentheses

A long-standing grammatical quirk in PHP required wrapping newly instantiated objects in parentheses before invoking methods or properties immediately (e.g. (new RequestBuilder())->build()). PHP 8.4 eliminates this awkward syntax:

php
123456789
<?php

// PHP 8.4: Direct method chaining on instantiation!
$response = new HttpClient()
    ->withTimeout(5)
    ->withBearerToken($token)
    ->get('https://api.draftsbook.com/v1/metrics');

$timestamp = new DateTimeImmutable()->format('Y-m-d H:i:s');

7. Standard Array Functions: array_find, array_any & array_all

Beyond OOP ergonomics, PHP 8.4 introduces long-awaited standard library array primitives that eliminate cumbersome functional boilerplate:

  • array_find(array $array, callable $callback): Returns the first element matching a predicate, or null if none is found. Replaces manual foreach break loops.
  • array_find_key(array $array, callable $callback): Returns the key of the first matching item.
  • array_any(array $array, callable $callback): Returns true if at least one item satisfies the condition.
  • array_all(array $array, callable $callback): Returns true if every single item satisfies the condition.
php
1234567891011121314
<?php

// High-performance predicate auditing with PHP 8.4 standard functions
$payments = [
    ['id' => 1, 'amount' => 500, 'status' => 'completed'],
    ['id' => 2, 'amount' => 1200, 'status' => 'failed'],
    ['id' => 3, 'amount' => 450, 'status' => 'completed'],
];

// Check if any payment failed in O(n) with instant short-circuit:
$hasFailure = array_any($payments, fn ($p) => $p['status'] === 'failed');

// Find the first high-value transaction exceeding 1000:
$flagged = array_find($payments, fn ($p) => $p['amount'] >= 1000);

8. Typed Class Constants & Asymmetric Inheritance

In earlier PHP versions, class constants could not be typed, permitting subclasses to inadvertently override a constant with an incompatible type. PHP 8.4 enforces strict typing on class constants:

php
12345678910111213
<?php

declare(strict_types=1);

namespace App\Domain\Contracts;

interface ApiSpecificationInterface
{
    // Typed class constants guarantee contract integrity:
    public const string PROTOCOL_VERSION = '2026.1';
    public const int DEFAULT_TIMEOUT_SECONDS = 30;
    public const array SUPPORTED_MIME_TYPES = ['application/json', 'application/problem+json'];
}

Furthermore, Asymmetric Visibility respects standard OOP inheritance covariance and contravariance rules. A subclass may widen read or write visibility (e.g. changing protected(set) to public), but it can never narrow visibility to break polymorphism.

9. PHP 8.4 JIT Compiler Tuning & Benchmark Profiling

PHP 8.4 refactors the Zend Just-In-Time (JIT) compilation engine. In previous versions, configuring JIT required obscure hexadecimal configuration flags. PHP 8.4 simplifies JIT configuration and optimizes machine code generation for long-running worker processes:

ini
1234567
; php.ini - High-Performance PHP 8.4 JIT Configuration
opcache.enable=1
opcache.enable_cli=1
opcache.jit=tracing
opcache.jit_buffer_size=128M
opcache.jit_hot_loop=64
opcache.jit_hot_func=16

Under Tracing JIT, the engine continuously inspects frequently executed execution paths (hot loops in serialization, cryptographic hashing, and array filtering). It compiles those bytecode sequences into raw x86_64 or ARM64 machine instructions, bypassing the Zend VM interpreter loop entirely and achieving up to 2.5x throughput improvements in CPU-bound operations.

6. Index Merge Optimization: Why Composite Indexes Win

When a query contains multiple WHERE conditions on columns that each possess standalone single-column indexes, MySQL attempts to use Index Merge (displayed in EXPLAIN as type: index_merge, Extra: Using intersect(...)).

While Index Merge is better than a full table scan, it incurs substantial CPU overhead because MySQL must traverse two separate B+Trees, load the row ID bitmaps into memory, calculate bitwise intersections or unions, and then perform clustered index lookups. Replacing multiple single-column indexes with one properly ordered composite index reduces execution time by up to 80% because all criteria are evaluated during a single tree traversal!

7. Native Descending Indexes in MySQL 9

In applications displaying paginated activity streams or chronological audit logs, queries frequently sort in descending order (ORDER BY created_at DESC). In MySQL 5.7 and older engines, indexes were always stored in ascending order; reading them in reverse required backward index scans, which are significantly slower due to InnoDB page pointer layout.

MySQL 9 fully supports Descending Indexes:

sql
123456789
-- Optimize reverse pagination with mixed sort orders:
CREATE INDEX idx_user_activity ON activity_logs (user_id ASC, created_at DESC);

-- Executes with zero filesort and optimal forward B+Tree traversal!
SELECT id, action, created_at
FROM activity_logs
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 20;

8. Invisible Indexes: Zero-Downtime Schema Evolution

Over years of feature development, databases accumulate unused or redundant indexes. Every index degrades write throughput because InnoDB must maintain every secondary B+Tree on every write.

However, dropping an index in production is risky: if an undocumented reporting query relied on it, dropping it causes an instant query stall. MySQL 9 provides Invisible Indexes:

sql
12345
-- Hide index from query optimizer without dropping the data:
ALTER TABLE blogs ALTER INDEX idx_old_legacy INVISIBLE;

-- If no slow queries are flagged in Prometheus/Grafana over 7 days:
ALTER TABLE blogs DROP INDEX idx_old_legacy;

Summary & Next Step

PHP 8.4 modernizes object-oriented engineering, enabling developers to write expressive, immutable, and strictly-typed domain entities with zero boilerplate. In Part 2, we examine how the enterprise application framework orchestrates these concepts: Laravel 13 Core Internals: Service Container, Pipelines & Terminating Middleware.

Share:
Share on X
Share on LinkedIn
Share on Facebook
Share on Reddit
Share on WhatsApp
Share on Telegram
Copy link

Related reading

PHP 8.4 Property Hooks & Asymmetric Visibility Guide · Draftsbook