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.
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
- ✓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
- 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.
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.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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).
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).
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.
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.
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.
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.
The Case of the Phantom Database Connection
- 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.
php -l file.phpvar_dump((new ReflectionProperty(Class::class, 'prop'))->getHooks());| File | Command / Code | Purpose |
|---|---|---|
| property_hooks.php | class User { | Understanding Property Hooks |
| asymmetric_visibility.php | class Order { | Asymmetric Visibility |
| lazy_objects.php | class DatabaseConnection { | Lazy Objects |
| combined.php | class Temperature { | Combining Features |
| lazy_with_hooks.php | class Config { | Lazy Objects and Property Hooks |
| migration.php | class User { | Migration Guide |
Key takeaways
Common mistakes to avoid
3 patternsUsing property hooks without a type declaration
Trying to set a property with private(set) from outside the class
Serializing an uninitialized lazy object
Interview Questions on This Topic
Explain the difference between a lazy ghost and a lazy proxy in PHP 8.4.
Frequently Asked Questions
20+ years shipping production PHP systems at scale. Drawn from code that ran under real load.
That's Advanced PHP. Mark it forged?
3 min read · try the examples if you haven't