Getters and Setters Without Boilerplate
For years, PHP developers wrote a pair of getX() and setX() methods for every validated or computed property. Property Hooks move that logic directly into the property declaration itself, with get and set hooks that behave like plain fields for callers while keeping full control over reads and writes.
Table of Contents
- 1. The Boilerplate Problem of Classic Getters/Setters
- 2. Property Hook Syntax: get and set
- 3. Virtual Properties Without a Backing Field
- 4. Validation and Transformation in the set Hook
- 5. Computed Values in the get Hook
- 6. Property Hooks in Interfaces and Abstract Classes
- 7. Inheritance: Overriding and Extending Hooks
- 8. Property Hooks vs. Classic Getter/Setter Methods
- 9. Limits and Pitfalls
- 10. Summary
- 11. FAQ
1. The Boilerplate Problem of Classic Getters/Setters
In practically every object-oriented PHP codebase you find the same pattern: a private property, a public getName() method that returns the value unchanged, and a setName() method that runs a validation before assigning it. For a class with ten properties, that quickly means twenty extra methods, most of which do nothing more than execute a single line of code. This boilerplate is not wrong, but it buries the actual business logic under ceremony that has to be rewritten for every new property.
The real problem is not the typing effort but maintainability. When a validation rule changes, it has to be updated in the setter method, and developers need to know that this method exists instead of assigning the property directly. In many projects this discipline is not consistently upheld: somewhere in the code, a legacy method reaches directly into a public property and bypasses any validation, because before PHP 8.4 there was no way to attach behavior to reading and writing a property itself without hiding it completely behind methods.
Property Hooks resolve exactly this tension. Instead of hiding a property and exposing it through methods, the property stays visible as a property and still gets behavior attached to it. The access $user->email = 'foo@bar.com' looks syntactically like a plain assignment, but can validate, normalize, or even compute the entire value under the hood, without the caller noticing anything or having to call a method. That not only cuts down the line count, it removes the temptation to accidentally set a property unchecked, because there simply is no unchecked access path left.
2. Property Hook Syntax: get and set
The basic syntax of Property Hooks adds a block in curly braces to the property declaration that can contain a get hook and a set hook. In the short arrow form it looks like this: public string $name { get => strtoupper($this->name); }. The expression after => is evaluated on every read access to the property. For more complex logic there is the block form with curly braces and an explicit return statement, which can contain any number of statements before the value is returned.
The set hook works symmetrically: it receives an implicit parameter named value representing the assigned value, and can check or transform it before the actual storage happens. Important detail: inside a hook, accessing $this->propertyName refers to the so-called backing field, meaning the value actually stored, not recursively to the hook itself. PHP recognizes this distinction automatically from the context, so get => $this->name works without any additional convention and without triggering an infinite loop.
A property can also define only one of the two hooks. A property with only a get hook effectively behaves like a computed, read-only property. A property with only a set hook keeps its normal read behavior but validates every assignment. This flexibility lets you implement exactly the level of control a given property actually needs, instead of covering both directions across the board.
declare(strict_types=1);
final class Product
{
public function __construct(
private string $name,
private float $priceNet,
) {
}
// Short-form get hook: normalizes on every read access
public string $name {
get => trim($this->name);
}
// Block-form set hook: validates and normalizes before storing
public float $priceNet {
set {
if ($value < 0.0) {
throw new InvalidArgumentException('Price must not be negative.');
}
$this->priceNet = round($value, 2);
}
}
}
$product = new Product(' Keyboard ', 49.995);
echo $product->name; // "Keyboard" - trimmed by the get hook
echo $product->priceNet; // 50.0 - rounded by the set hook
3. Virtual Properties Without a Backing Field
A particularly useful application of Property Hooks is virtual properties, meaning properties that have no stored value of their own at all. As soon as a property defines only a get hook and, inside that hook, does not reference a backing field but computes the value entirely from other properties, no storage exists for that property inside the object. In that case PHP does not allocate a backing field, which saves memory and, at the same time, rules out returning a stale, cached value.
A typical example is a fullName property composed from firstName and lastName. Instead of maintaining a separate instance variable that would need to stay in sync every time the first or last name changes, the get hook recomputes the value on every access. That eliminates an entire class of synchronization bugs where a derived property is forgotten after a change to its source properties.
Virtual properties also suit values derived from external sources, such as a formatted or display value computed from an internal raw value. The important distinction: a virtual property with a pure get hook is inherently read-only. Attempting to assign it a value causes PHP to throw an error, since there is no set hook and no backing field to write into. This clarity is an advantage over classic getters, where the absence of a write path is not visible from the signature but only inferred from the absence of a setter method.
declare(strict_types=1);
final class Customer
{
public function __construct(
private string $firstName,
private string $lastName,
) {
}
// Virtual property: no backing field, computed on every access
public string $fullName {
get => sprintf('%s %s', $this->firstName, $this->lastName);
}
// Virtual, derived, read-only presentation value
public string $initials {
get => strtoupper($this->firstName[0] . $this->lastName[0]);
}
}
$customer = new Customer('Ada', 'Lovelace');
echo $customer->fullName; // "Ada Lovelace" - computed, never stored
echo $customer->initials; // "AL"
// $customer->fullName = 'New Name'; // Error: no set hook defined
4. Validation and Transformation in the set Hook
Arguably the most common use case for Property Hooks in practice is validating input data directly at assignment time. Instead of declaring a private property and writing a separate setEmail() method that handles validation, the check moves directly into the property's set hook. Every assignment, whether through constructor property promotion or later in the code, automatically goes through the same validation logic, without any developer needing to remember to use the correct method instead of a direct assignment.
Besides pure validation that throws an exception on an invalid value, the set hook is equally well suited for normalization. A phone number field can automatically strip whitespace and hyphens on assignment, an email field can be lowercased before the value lands in the backing field. This transformation happens transparently for the caller: it assigns a raw value and later reads back the normalized value, without ever calling a separate normalization function.
An important detail is the ability to declare the type of the implicit value parameter in the set hook more broadly or differently than the property's own declared type, for instance to additionally accept Stringable objects alongside a string and normalize them to a plain string inside the hook. That allows a more permissive, developer-friendly API on the outside while internally only ever storing the strictly typed, validated value. This combination of leniency on input and strictness on stored state simply was not expressible with classic property declarations without an extra setter method.
declare(strict_types=1);
final class EmailAddress
{
public string $value {
set {
$normalized = strtolower(trim($value));
if (!filter_var($normalized, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
sprintf('"%s" is not a valid email address.', $value)
);
}
$this->value = $normalized;
}
}
public function __construct(string $value)
{
$this->value = $value; // Runs through the set hook above
}
}
$email = new EmailAddress(' User@Example.COM ');
echo $email->value; // "user@example.com" - validated and normalized
5. Computed Values in the get Hook
While virtual properties do without a backing field entirely, there is a second, equally common case: a property has a backing field, but the get hook transforms the stored raw value on every read access. One example is an internal amount stored in cents that is returned as a formatted euro amount with two decimal places on read, while the internal representation keeps using integer cent values to avoid floating-point rounding errors.
An important trap with computed get hooks concerns performance and caching. Since the hook runs on every single read access, an expensive computation, such as heavy string processing or even a database call, becomes correspondingly costly inside a loop with many read accesses. Property Hooks provide no built-in memoization, the result is not automatically cached. Anyone who wants to cache the result has to implement that explicitly, for example with an additional nullable property that gets populated on first access.
A second point that is easy to miss: a get hook that depends on other, mutable properties must account for that dependency freshly on every evaluation. If one of the source properties changes between two read accesses, the get hook automatically returns the current, freshly computed value on the next access. In most cases that is exactly the desired behavior, but it can surprise a developer who unconsciously assumes a one-time computed, immutable value, even though the property is re-evaluated on every single access. A typical example is an internal cent amount with a formattedTotal hook that gets reformatted on every read: without manual caching through an additional private property, number_format() runs unnecessarily often on frequent reads, which can become noticeable inside rendering loops with many list items.
6. Property Hooks in Interfaces and Abstract Classes
Property Hooks can be used not only in concrete classes but also declared as a contract in interfaces. An interface can require a property with { get; } without an implementation, ensuring that every implementing class provides a readable property of that name and type, regardless of whether the concrete class implements an actual hook or uses a plain, unhooked property that automatically satisfies the same contract.
This is a meaningful difference from classic getters in interfaces: instead of requiring public function getName(): string;, the interface directly declares public string $name { get; }. For the caller, access changes noticeably: it reads $object->name instead of calling $object->getName(), which, especially combined with value objects and data transfer objects, produces noticeably more readable code without losing any of the guarantees an interface provides.
In abstract classes this principle can be extended further: an abstract class can declare an abstract get hook that concrete subclasses must implement, while providing a concrete set hook itself with shared validation logic for all subclasses. This split lets you centralize shared behavior once while still enforcing subclass-specific behavior, much like abstract methods, just applied at the level of individual properties rather than whole methods.
declare(strict_types=1);
interface HasDisplayLabel
{
// Contract: any implementing class must expose a readable label
public string $displayLabel { get; }
}
abstract class AbstractStatus implements HasDisplayLabel
{
protected string $code = '';
// Shared validation for all subclasses, enforced through the set hook
public string $code {
set {
if ($value === '') {
throw new InvalidArgumentException('Status code must not be empty.');
}
$this->code = strtoupper($value);
}
}
// Forces every concrete subclass to define its own label computation
abstract public string $displayLabel { get; }
}
final class OrderStatus extends AbstractStatus
{
public string $displayLabel {
get => match ($this->code) {
'NEW' => 'Newly received',
'SHIPPED' => 'Shipped',
default => 'Unknown',
};
}
}
7. Inheritance: Overriding and Extending Hooks
When inheriting from classes with Property Hooks, a subclass can override an inherited hook to add extra behavior without fully duplicating the base logic. The key to this is access via parent::$propertyName, which lets a subclass reach specifically into the parent class's backing field while the overriding hook adds its own additional checks or transformations. Conceptually this mirrors the parent::method() call for overridden methods, just applied to property access.
Regarding covariance, Property Hooks follow rules similar to method return types: an overriding get hook is allowed to return a narrower, more specific type than the one originally declared in the parent class, as long as that type remains compatible with the parent type. For the set hook the opposite contravariance rule applies: the accepted value type must not become narrower when overriding, otherwise the base class contract that any value valid there can also be assigned in the subclass would be violated.
A practical example is a base class Money that, in its set hook, only checks that a value is not negative, while a subclass ForeignMoney additionally adds a currency validation but still uses parent::$amount = $value; to reuse the base check and avoid code duplication. This combination of reuse and targeted extension was also possible with classic setter methods via a call to parent::setAmount($value), but feels more consistent with Property Hooks because both the base and the subclass use the same access syntax through the property itself.
declare(strict_types=1);
class Money
{
protected float $amount = 0.0;
public float $amount {
set {
if ($value < 0.0) {
throw new InvalidArgumentException('Amount must not be negative.');
}
$this->amount = $value;
}
}
}
final class ForeignMoney extends Money
{
public function __construct(
private readonly string $currency,
) {
}
// Extends the parent hook instead of duplicating the base validation
public float $amount {
set {
if (!in_array($this->currency, ['USD', 'GBP', 'CHF'], true)) {
throw new InvalidArgumentException('Unsupported currency.');
}
parent::$amount = $value; // Reuses the base class validation
}
}
}
$payment = new ForeignMoney('USD');
$payment->amount = 42.50; // Runs currency check, then the inherited negativity check
8. Property Hooks vs. Classic Getter/Setter Methods
Performance-wise there is no practically relevant difference between a get hook and a classic getter method: both are ultimately executed by the PHP engine as a method call with comparable overhead, a Property Hook is not a pure field-access optimization at the bytecode level. The real advantage lies not in execution speed but in the readability of calling code, and in the fact that call sites written as $obj->prop instead of $obj->getProp() are noticeably more compact and read more naturally in templates or expressions.
Migrating existing classes with an established getX()/setX() API raises a practical compatibility question: an already published library cannot simply replace its public methods with Property Hooks without breaking the existing API, since callers still expect ->getName(). One viable path is to introduce Property Hooks alongside the existing method, where the method internally just reads or writes the property, so new callers can use the more compact property syntax while old callers keep working unchanged.
It is also worth distinguishing Property Hooks from two related PHP 8.4 features: Asymmetric Visibility (public private(set) string $name) governs who may write a property at all, without inserting behavior such as validation, and Readonly Properties prevent any change after initialization entirely. Property Hooks differ from both because they do not govern visibility or mutability, but determine the actual behavior on every read or write access. All three features can be combined in PHP 8.4, but they each address a different question.
| Scenario | Classic Approach (Getter/Setter Method) | Property Hook | Advantage |
|---|---|---|---|
| Validation on assignment | setEmail(string $v) called manually |
set { ...; $this->x = $v; } |
Direct assignment can no longer bypass it |
| Computed value | getFullName(): string |
get => "$first $last" |
Accessed like a plain field |
| Virtual property | Method with no backing field needed | get-only, no storage |
No dead storage inside the object |
| Contract in an interface | public function getX(): string; |
public string $x { get; } |
Shorter, clearer contract |
| Extension in a subclass | parent::setX($v) |
parent::$x = $v; |
Consistent access syntax |
9. Limits and Pitfalls
An important limitation of Property Hooks is that one hook cannot directly access another hook of the same or a different property to reuse its logic. There is no syntax to "call the get hook of another property" other than the regular property access itself. Anyone who wants to share logic between multiple hooks has to extract it into a private method and call it from both hooks, instead of expecting a direct hook-to-hook reference, which simply does not exist in PHP 8.4.
Recursion is a real danger if a get hook accidentally references itself instead of the backing field. As long as the hook accesses only its own backing field via $this->propertyName, PHP behaves correctly and no infinite loop occurs. But if another hook is accidentally invoked that in turn reads the first one, a circular dependency can form that PHP does not detect automatically at compile time, and it only surfaces at runtime as a stack overflow error.
There are also special considerations when interacting with serialization and debugging functions. var_dump() and print_r() do not show a stored raw value for virtual properties without a backing field, and may either trigger the get hook or mark the property as virtual, depending on the PHP version and the debugging tools used. With serialize() and the Serializable interface, only backing fields that actually exist get serialized, virtual properties are recomputed from the available fields on deserialization and therefore do not need separate handling, as long as their dependencies are themselves serialized. A simple example illustrates the backing-field rule: a property public int $value { get => $this->value; set { $this->value += $value; } } correctly accesses its own backing field in both hooks and creates no recursion, as long as no other hook is interposed that in turn triggers the same access.
10. Summary
Property Hooks solve a problem PHP developers have worked around with manual boilerplate for years: validation, normalization, and computed values can now live directly in the property declaration instead of being hidden behind separate getX()/setX() methods. The get hook allows both transformed read access to an existing backing field and fully virtual, unstored properties. The set hook centralizes validation and transformation in exactly one place that cannot be bypassed.
For larger codebases, the possibilities within interfaces and inheritance are especially relevant: an interface can define a clear property contract with { get; }, and subclasses can reach into base behavior with parent::$prop instead of duplicating it. At the same time, real limits remain, such as the lack of direct hook-to-hook communication and the need to implement caching yourself for expensive get hooks. Anyone aware of these limits can deploy Property Hooks precisely where they offer real value over classic getters and setters.
Property Hooks in PHP 8.4 - The Key Takeaways
Syntax
public T $x { get => ...; set { ...; $this->x = $value; } }, short or block form per hook.
Virtual Properties
A pure get hook without backing field access produces a computed, read-only property with no storage.
Inheritance
parent::$prop reaches into the parent class's backing field, hooks can be extended in a targeted way instead of duplicated.
Limits
No automatic caching in the get hook, no direct access to other hooks, risk of recursion on incorrect field access.
11. FAQ: Property Hooks in PHP 8.4
1What are Property Hooks in PHP 8.4?
2How do Property Hooks differ from classic getters and setters?
3What is a virtual property?
4How do I access the stored value inside a hook?
5Can Property Hooks be declared in interfaces?
6How can a subclass extend an inherited Property Hook?
7Do Property Hooks automatically cache computed values?
8What distinguishes Property Hooks from Asymmetric Visibility?
9What distinguishes Property Hooks from Readonly Properties?
10Can one hook access another hook of the same class?
Mironsoft
PHP 8.4 Code Review and Modernization
Modernizing existing PHP code with Property Hooks?
We review existing getter/setter boilerplate in your codebase, identify good candidates for Property Hooks, and guide the step-by-step migration to PHP 8.4, without breaking existing APIs.
Code Review
Analysis of existing getter/setter patterns and assessment of migration potential to Property Hooks
Modernization
Step-by-step introduction of Property Hooks, Asymmetric Visibility and Readonly Properties in PHP 8.4
PHPStan Coverage
Static analysis at level 5+ after migration, so type safety and contracts remain intact