Home PHP PHP 8.4: Property Hooks, Asymmetric Visibility, and Lazy Objects Explained
Intermediate 3 min · July 13, 2026

PHP 8.4: Property Hooks, Asymmetric Visibility, and Lazy Objects Explained

Master PHP 8.4's property hooks, asymmetric visibility, and lazy objects with practical examples.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic understanding of PHP classes and objects
  • Familiarity with PHP 8.0+ features like typed properties and readonly properties
  • PHP 8.4 installed or access to a PHP 8.4 environment
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Property hooks allow custom logic when getting/setting properties without breaking encapsulation.
  • Asymmetric visibility lets you set different access levels for read and write operations.
  • Lazy objects defer initialization until first use, improving performance.
  • These features reduce boilerplate and make code more expressive.
  • They are backward compatible and can be adopted incrementally.
✦ Definition~90s read
What is PHP 8.4?

PHP 8.4 introduces property hooks, asymmetric visibility, and lazy objects to give you more control over property access and object initialization, making your code cleaner and more efficient.

Think of a property like a mailbox.
Plain-English First

Think of a property like a mailbox. Normally, anyone can put mail in or take mail out. Property hooks are like having a smart mailbox that logs every letter or checks if the sender is allowed. Asymmetric visibility is like having a mailbox where anyone can see if there's mail (public read), but only you have the key to open it (private write). Lazy objects are like a vending machine that only restocks when someone presses a button, saving energy when idle.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

PHP 8.4 introduces three powerful features that change how you design and interact with objects: property hooks, asymmetric visibility, and lazy objects. These features address long-standing pain points in PHP development, such as boilerplate getter/setter methods, inconsistent access control, and unnecessary object initialization. Property hooks allow you to add logic to property access without breaking the public API, making your code more maintainable and expressive. Asymmetric visibility gives you fine-grained control over who can read vs. write a property, eliminating the need for separate getter/setter methods with different visibility. Lazy objects defer the creation and initialization of objects until they are actually needed, which can significantly improve performance in applications with many optional dependencies or heavy objects. In this tutorial, you'll learn each feature with real-world examples, best practices, and debugging tips. By the end, you'll be able to refactor legacy code and design new systems that are cleaner, faster, and more secure.

Understanding Property Hooks

Property hooks allow you to define custom behavior when a property is read or written, without breaking the public API. Before PHP 8.4, you had to use getter and setter methods, which cluttered the interface. Now you can attach hooks directly to properties. The syntax uses 'get' and 'set' keywords after the property declaration. The get hook runs when the property is read, and the set hook runs when it's written. Inside the set hook, you can use the special variable '$value' to refer to the incoming value. You can also omit the set hook to make a property read-only (but still writable internally if you use asymmetric visibility). Property hooks are especially useful for validation, logging, lazy loading, and computed properties. They work with typed properties and can be combined with asymmetric visibility. Note that hooks cannot be used with static properties or properties in interfaces (yet).

property_hooks.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

class User {
    public string $name {
        get => ucfirst($this->name);
        set => strtolower($value);
    }

    public function __construct(string $name) {
        $this->name = $name;
    }
}

$user = new User('john');
echo $user->name; // Outputs: John
$user->name = 'JANE';
echo $user->name; // Outputs: Jane
Output
John
Jane
💡Property Hooks vs. Magic Methods
📊 Production Insight
Be careful with infinite recursion in get hooks: if you access the property inside its own get hook, you'll get a stack overflow. Use a backing field (e.g., $this->name) carefully.
🎯 Key Takeaway
Property hooks let you add logic to property access without changing the public API, making code cleaner and more maintainable.

Asymmetric Visibility: Read vs. Write Access

Asymmetric visibility allows you to specify different access levels for reading and writing a property. The syntax is 'public private(set)' meaning anyone can read, but only the class itself can write. You can also use 'protected private(set)' or 'public protected(set)'. This is useful for value objects or DTOs where you want to expose data but prevent external modification. It replaces the common pattern of having a public getter and a private setter. Asymmetric visibility can be combined with readonly properties (readonly implies public private(set) by default). Note that asymmetric visibility only affects the property's visibility; you can still have hooks that run on set. Also, the write visibility cannot be wider than the read visibility (e.g., you cannot have private read and public write).

asymmetric_visibility.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class Order {
    public private(set) string $status = 'pending';

    public function approve(): void {
        $this->status = 'approved';
    }
}

$order = new Order();
echo $order->status; // Outputs: pending
// $order->status = 'shipped'; // Error: Cannot modify readonly property
$order->approve();
echo $order->status; // Outputs: approved
Output
pending
approved
🔥Readonly Properties
📊 Production Insight
When using asymmetric visibility with inheritance, remember that child classes inherit the read visibility but cannot change the write visibility. Use 'protected private(set)' if you want child classes to read but not write.
🎯 Key Takeaway
Asymmetric visibility provides fine-grained control over property access, reducing boilerplate and improving encapsulation.

Lazy Objects: Deferred Initialization

Lazy objects are objects that are not fully initialized until a property or method is accessed. PHP 8.4 introduces two types: lazy ghosts and lazy proxies. A lazy ghost is an object that is initially empty and becomes the real object upon first access. A lazy proxy is a separate proxy object that forwards calls to the real object after initialization. To create a lazy ghost, use ReflectionClass::newLazyGhost with a callable initializer. The initializer receives the object instance and should set its properties. Lazy objects are ideal for expensive-to-create objects like database connections, API clients, or large configuration objects. They can significantly reduce memory usage and startup time. However, they add a small overhead on first access. Use them judiciously.

lazy_objects.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

class DatabaseConnection {
    public function __construct(private string $dsn) {
        // Simulate heavy initialization
        sleep(1);
        echo "Connected to $dsn\n";
    }

    public function query(string $sql): string {
        return "Result for: $sql";
    }
}

$reflector = new ReflectionClass(DatabaseConnection::class);
$connection = $reflector->newLazyGhost(function (DatabaseConnection $object) {
    // Initializer: called when first accessed
    $object->__construct('mysql:host=localhost;dbname=test');
});

echo "Object created, but not yet initialized.\n";
sleep(2);
echo $connection->query('SELECT 1'); // Triggers initialization
Output
Object created, but not yet initialized.
Connected to mysql:host=localhost;dbname=test
Result for: SELECT 1
⚠ Lazy Object Pitfalls
📊 Production Insight
Use lazy objects for services that are injected but not always used, such as in dependency injection containers. Profile to ensure the overhead of lazy initialization is worth it.
🎯 Key Takeaway
Lazy objects defer costly initialization until needed, improving performance and resource usage.

Combining Features: Property Hooks with Asymmetric Visibility

You can combine property hooks and asymmetric visibility for powerful effects. For example, you might have a property that is publicly readable but only privately settable, and you want to validate the value on set. The syntax is straightforward: declare the asymmetric visibility first, then the hook. The set hook will run when the property is written internally. This combination is perfect for value objects that need validation. It also allows you to expose computed properties that are derived from other data. Remember that the get hook can also be used to transform the value on read. This pattern reduces the need for separate getter/setter methods and keeps the logic co-located with the property.

combined.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

class Temperature {
    public private(set) float $celsius {
        set {
            if ($value < -273.15) {
                throw new InvalidArgumentException('Temperature below absolute zero');
            }
            $this->celsius = $value;
        }
    }

    public float $fahrenheit {
        get => $this->celsius * 9 / 5 + 32;
    }

    public function __construct(float $celsius) {
        $this->celsius = $celsius;
    }
}

$temp = new Temperature(25);
echo $temp->celsius; // 25
echo $temp->fahrenheit; // 77
// $temp->celsius = 30; // Error: Cannot modify readonly property
Output
25
77
💡Computed Properties
📊 Production Insight
When using computed properties, ensure they are idempotent and have no side effects. Otherwise, they can cause unexpected behavior.
🎯 Key Takeaway
Combining asymmetric visibility with property hooks gives you full control over property access and mutation logic.

Lazy Objects and Property Hooks: A Perfect Match

Lazy objects can benefit from property hooks to add logging or validation when properties are accessed. For example, you might have a lazy-loaded configuration object that logs every access for debugging. You can define property hooks on the class that will be used after initialization. However, note that hooks are not called during the initialization of a lazy ghost (the initializer sets properties directly). After initialization, hooks work normally. This allows you to have lazy objects with smart properties. Another use case is to make a property lazily computed: you can use a get hook that triggers initialization of a related lazy object. This pattern is powerful for building complex object graphs that are loaded on demand.

lazy_with_hooks.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

class Config {
    public private(set) array $data {
        get {
            echo "Accessing config data\n";
            return $this->data;
        }
    }

    public function __construct() {
        // Heavy initialization
        $this->data = ['db' => 'mysql', 'cache' => 'redis'];
    }
}

$reflector = new ReflectionClass(Config::class);
$config = $reflector->newLazyGhost(function (Config $object) {
    $object->__construct();
});

echo "Before access\n";
$data = $config->data; // Triggers initialization and hook
echo "After access\n";
Output
Before access
Accessing config data
After access
🔥Lazy Ghost vs. Lazy Proxy
📊 Production Insight
Be aware that property hooks on lazy objects run after initialization. If you need to hook into initialization itself, consider using an initializer callback.
🎯 Key Takeaway
Combining lazy objects with property hooks allows you to build efficient, self-logging, or validating systems.

Migration Guide: Refactoring Legacy Code

Migrating existing code to use PHP 8.4 features should be done incrementally. Start by identifying properties that have getter/setter methods. Replace them with property hooks, ensuring backward compatibility. For asymmetric visibility, look for properties that are exposed via public getters but have private setters. Replace with public private(set). For lazy objects, identify heavy objects that are created early but used rarely. Use lazy ghosts or proxies to defer initialization. Always run your test suite after changes. Note that property hooks cannot be added to interfaces yet, so you may need to keep interfaces unchanged. Also, be careful with serialization: lazy objects may not serialize correctly before initialization. Consider using the 'lazy' keyword in docblocks to document the behavior.

migration.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

// Before PHP 8.4
class User {
    private string $name;

    public function getName(): string {
        return ucfirst($this->name);
    }

    public function setName(string $name): void {
        $this->name = strtolower($name);
    }
}

// After PHP 8.4
class User {
    public string $name {
        get => ucfirst($this->name);
        set => strtolower($value);
    }
}
⚠ Backward Compatibility
📊 Production Insight
Use static analysis tools like PHPStan or Psalm to catch issues with new features. They may not fully support PHP 8.4 yet, so test thoroughly.
🎯 Key Takeaway
Migrate incrementally: replace getters/setters with hooks, then add asymmetric visibility, then introduce lazy objects where beneficial.
● Production incidentPOST-MORTEMseverity: high

The Case of the Phantom Database Connection

Symptom
Application response times increased by 300% after a refactor. Database connections spiked, causing connection pool exhaustion.
Assumption
The developer assumed that accessing a property on a User object would be cheap, as it only returned a cached value.
Root cause
The User object's constructor eagerly initialized a DatabaseConnection object, which was stored in a property. Even though the property was never used on most pages, the connection was always established.
Fix
Replaced eager initialization with a lazy object using PHP 8.4's lazy object feature. The DatabaseConnection is now only created when the property is first accessed.
Key lesson
  • Always consider the cost of object initialization, especially for heavy resources like database connections.
  • Use lazy objects for properties that may not be needed on every request.
  • Profile your application to identify unnecessary object creation.
  • Document which properties are lazily loaded to avoid confusion.
  • Consider using property hooks to add logging or validation when accessing lazy properties.
Production debug guideSymptom to Action3 entries
Symptom · 01
Property hook getter throws an exception unexpectedly
Fix
Check the hook logic for side effects or dependencies that may fail. Use try-catch inside the hook to handle errors gracefully.
Symptom · 02
Asymmetric visibility causes 'Cannot modify readonly property' error
Fix
Verify that the write visibility is set correctly. Remember that private(set) means only the class can write, not subclasses.
Symptom · 03
Lazy object initialization never happens
Fix
Ensure that the lazy object is actually accessed. Check if the property is read before use. Also verify that the initializer function is correct.
★ Quick Debug Cheat SheetCommon issues and immediate fixes for PHP 8.4 features.
Property hook not called
Immediate action
Check if property is typed and hook syntax is correct (get => ...).
Commands
php -l file.php
var_dump((new ReflectionProperty(Class::class, 'prop'))->getHooks());
Fix now
Ensure property has a type declaration and hook uses 'get' or 'set' keyword.
Asymmetric visibility error+
Immediate action
Check the visibility modifiers on the property.
Commands
php -r 'echo (new ReflectionProperty(Class::class, "prop"))->isPublic() ? "public" : "private";'
php -r 'echo (new ReflectionProperty(Class::class, "prop"))->isReadOnly() ? "readonly" : "";'
Fix now
Change to public private(set) or protected private(set) as needed.
Lazy object not lazy+
Immediate action
Check if object is created with ReflectionClass::newLazyGhost or newLazyProxy.
Commands
php -r 'var_dump((new ReflectionClass(Class::class))->isLazy());'
php -r 'var_dump((new ReflectionClass(Class::class))->getLazyInitializer());'
Fix now
Use ReflectionClass::newLazyGhost with a proper initializer.
FeatureBefore PHP 8.4After PHP 8.4Benefit
Property hooksGetter/setter methodsget/set hooks on propertyLess boilerplate, clearer intent
Asymmetric visibilityPublic getter, private setterpublic private(set) propertySingle declaration, no extra methods
Lazy objectsManual lazy loading patternsReflectionClass::newLazyGhost/proxyStandardized, efficient lazy initialization
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
property_hooks.phpclass User {Understanding Property Hooks
asymmetric_visibility.phpclass Order {Asymmetric Visibility
lazy_objects.phpclass DatabaseConnection {Lazy Objects
combined.phpclass Temperature {Combining Features
lazy_with_hooks.phpclass Config {Lazy Objects and Property Hooks
migration.phpclass User {Migration Guide

Key takeaways

1
Property hooks reduce boilerplate by attaching logic directly to properties.
2
Asymmetric visibility provides fine-grained access control without separate methods.
3
Lazy objects defer initialization, improving performance and memory usage.
4
Combine these features for cleaner, more efficient code.
5
Migrate incrementally and test thoroughly.

Common mistakes to avoid

3 patterns
×

Using property hooks without a type declaration

×

Trying to set a property with private(set) from outside the class

×

Serializing an uninitialized lazy object

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between a lazy ghost and a lazy proxy in PHP 8.4.
Q02SENIOR
How would you implement a read-only property that logs every read access...
Q03SENIOR
What are the limitations of asymmetric visibility?
Q01 of 03SENIOR

Explain the difference between a lazy ghost and a lazy proxy in PHP 8.4.

ANSWER
A lazy ghost is an instance of the real class that becomes fully initialized upon first access. It is created using ReflectionClass::newLazyGhost. A lazy proxy is a separate object that implements the same interface and forwards calls to the real object after initialization. Ghosts are simpler but cannot be used with final classes; proxies work with interfaces and allow more control.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I use property hooks with readonly properties?
02
Are lazy objects serializable?
03
Can I use asymmetric visibility with static properties?
04
Do property hooks work with interfaces?
05
What is the performance impact of lazy objects?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
PHP Memory Management
14 / 29 · Advanced PHP
Next
PHP 8.3: Override Attribute, json_validate, and Dynamic Constant Fetch