Objects Namespaces And Application Architecture
Property Hooks
Property hooks let a PHP property run code when it is read or written. They give some of the control of getters and setters while keeping property syntax.
Hooks are useful for small, local rules such as validation, normalisation, and computed values. They should not hide expensive work, database queries, HTTP calls, or surprising side effects behind simple-looking property access.
Set Hooks
A set hook runs when a value is assigned to a property.
<?php
declare(strict_types=1);
final class UserProfile
{
public string $email {
set {
$email = strtolower(trim($value));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email address is not valid.');
}
$this->email = $email;
}
}
}
$profile = new UserProfile();
$profile->email = ' Ada@Example.COM ';
echo $profile->email . PHP_EOL;
// Prints:
// ada@example.com
The assignment still looks like property assignment, but the hook trims, lowercases, and validates before storing the value.
Get Hooks
A get hook runs when a property is read. It can return a computed value.
<?php
declare(strict_types=1);
final class PersonName
{
public function __construct(
public string $firstName,
public string $lastName,
) {
}
public string $fullName {
get => $this->firstName . ' ' . $this->lastName;
}
}
$name = new PersonName('Ada', 'Lovelace');
echo $name->fullName . PHP_EOL;
// Prints:
// Ada Lovelace
This is clearer than storing a separate $fullName property that can fall out of sync.
Hooks Versus Getter Methods
Property hooks are best when the operation feels like property access. A computed display name or a normalised email property can be reasonable.
Use a method when the operation sounds like work:
calculateTotal()loadInvoices()sendEmail()refreshFromApi()
Reading $object->total should not unexpectedly query a database or call a remote service.
Constructor Promotion And Hooks
Promoted properties can use hooks too.
<?php
declare(strict_types=1);
final class Tag
{
public function __construct(
public string $name {
set {
$name = trim($value);
if ($name === '') {
throw new InvalidArgumentException('Tag name cannot be empty.');
}
$this->name = $name;
}
},
) {
}
}
$tag = new Tag(' php ');
echo $tag->name . PHP_EOL;
// Prints:
// php
This keeps the property declaration and its assignment rule together.
Hooks Are Not A Replacement For Good Design
Hooks can make code cleaner, but overusing them creates hidden behaviour. If every property runs complex code, reading the class becomes harder.
Good hook use is:
- local to the property
- quick to execute
- easy to explain
- free of external side effects
- tested with normal and invalid values
Bad hook use hides business workflows, persistence, or network calls behind assignment.
PHP Version And Deployment
Property hooks were introduced in PHP 8.4. They are syntax understood by the parser, so code containing hooks cannot be loaded on PHP 8.3 or earlier. Libraries must raise their Composer PHP requirement before publishing hooked properties.
Hooks apply to non-static properties. A property may be typed or untyped and can define get, set, or both. Static properties cannot use hooks because the feature is designed around access to an object instance and its scope.
Do not add hooks merely to make a codebase appear current. They are useful when property syntax accurately describes the operation and the behavior remains local and unsurprising.
Backed And Virtual Properties
A backed property stores a value. A hooked property is backed when at least one hook refers directly to that same property using syntax such as $this->email.
<?php
declare(strict_types=1);
final class Username
{
public string $value = '' {
set => strtolower(trim($value));
}
}
$username = new Username();
$username->value = ' Ada_L ';
echo $username->value . PHP_EOL;
// Prints:
// ada_l
The short set expression returns the value PHP stores in the backing property. Omitting get leaves normal read behavior in place for this backed property.
A virtual property has no storage of its own. Its hooks calculate or redirect behavior without directly referencing that property.
<?php
declare(strict_types=1);
final class Rectangle
{
public function __construct(
public float $width,
public float $height,
) {
}
public float $area {
get => $this->width * $this->height;
}
}
$rectangle = new Rectangle(4.5, 2.0);
echo $rectangle->area . PHP_EOL;
// Prints:
// 9
$area consumes no separate property storage and cannot fall out of sync with width and height. Because the virtual property defines no set, assigning to $rectangle->area is an error.
Whether a property is backed is determined by an exact direct reference to itself. Dynamic access such as $this->{$name} does not count as backing and can produce an error if it tries to read the virtual property recursively.
Full And Short Hook Syntax
Use the arrow form for one expression:
<?php
class Article
{
public string $title {
get => strtoupper($this->title);
set => trim($value);
}
}
Use a block when validation, several statements, or explicit control flow improves clarity:
<?php
declare(strict_types=1);
final class StockItem
{
public int $quantity = 0 {
set {
if ($value < 0) {
throw new InvalidArgumentException('Quantity cannot be negative.');
}
$this->quantity = $value;
}
}
}
In a set hook, $value is the incoming value when no explicit parameter is declared. An explicit set parameter can have the property type or a wider contravariant type.
<?php
declare(strict_types=1);
final class PublishedAt
{
public DateTimeImmutable $value {
set (string|DateTimeImmutable $value) {
$this->value = is_string($value)
? new DateTimeImmutable($value)
: $value;
}
}
}
$date = new PublishedAt();
$date->value = '2026-06-10';
echo $date->value->format('Y-m-d') . PHP_EOL;
// Prints:
// 2026-06-10
Widening can be convenient, but it makes assignment behavior less obvious. Use it when accepting multiple input representations is genuinely part of the public property contract.
Read And Write Operations Can Differ
A get-only virtual property is read-only because no set operation exists. A set-only virtual property may route writes elsewhere but cannot be read. Backed properties retain normal behavior for a missing hook.
PHP 8.4 also supports asymmetric property visibility. A property can be publicly readable but writable only from private or protected scope, for example public private(set) string $status. This controls who may write; a hook controls what happens during the write. They solve related but different problems.
Property hooks are incompatible with readonly properties. If a hooked property needs restricted writes, use asymmetric set visibility and enforce the intended transition in the hook or methods. Readonly's one-time assignment model does not combine with intercepted access.
Constructor Promotion Has A Type Limitation
Promoted properties can define hooks, as the earlier Tag example shows. The constructor parameter, however, uses the declared property type even when the set hook accepts a wider type.
If a promoted property is declared DateTimeInterface and its set hook accepts string|DateTimeInterface, ordinary assignment may accept a string, but the promoted constructor parameter still accepts only DateTimeInterface. Use an ordinary constructor parameter and explicit assignment when construction should accept the wider input.
This distinction follows from promotion creating both a parameter and a property. The parameter signature is based on the property declaration, not the set hook's contravariant input type.
Hooks Run In Object Scope
A hook can access private methods and properties of the same object. It may delegate a focused validation or normalization rule to a private method.
<?php
declare(strict_types=1);
final class Contact
{
public string $phone {
set => $this->normalisePhone($value);
}
private function normalisePhone(string $phone): string
{
$digits = preg_replace('/\D+/', '', $phone);
if ($digits === null || strlen($digits) < 10) {
throw new InvalidArgumentException('Phone number is too short.');
}
return $digits;
}
}
$contact = new Contact();
$contact->phone = '+44 (0)20 1234 5678';
echo $contact->phone . PHP_EOL;
// Prints:
// 4402012345678
Accessing another hooked property from a hook runs that other property's hooks normally. This can create chains of hidden behavior or recursion. Keep dependencies between hooks simple and test them directly.
Avoid Hidden I/O And Expensive Work
Property access looks cheap. A developer reading $invoice->total does not expect a database query, an HTTP request, a filesystem scan, or a message to be sent.
Use hooks for operations such as:
- trimming and case normalization
- local validation
- converting a small value representation
- deriving a value from state already in memory
- exposing an alias with a stable meaning
Use methods for work such as refreshExchangeRate(), loadOrders(), calculateRemoteQuote(), or sendReceipt(). Methods signal that an operation may fail, take time, cause side effects, or need explicit parameters.
A hook that logs every property read can also create noisy, surprising behavior during debugging and serialization. Observability belongs at meaningful application boundaries rather than every simple access.
Array Properties And Indirect Modification
Hooks intercept whole-property reads and writes. Indirect modifications involve references and need special care. Code such as $object->tags[] = 'php' attempts to modify an element through the property's returned value and can bypass a set hook.
For a backed array property with hooks, element modification is not generally available through an ordinary get. Replacing the whole array is a normal set operation:
<?php
declare(strict_types=1);
final class Article
{
/** @var list<string> */
public array $tags = [] {
set {
$clean = array_values(array_unique(array_map('trim', $value)));
$this->tags = $clean;
}
}
}
$article = new Article();
$article->tags = [...$article->tags, 'php', ' php '];
echo implode(', ', $article->tags) . PHP_EOL;
// Prints:
// php
A by-reference &get hook exists for specialized reference behavior, but it has restrictions and can make validation easier to bypass. Prefer explicit collection methods such as addTag() when element-level mutation is central to the model.
Inheritance And Final Hooks
A child class may override individual hooks much like methods. It may add a hook to an inherited property or redefine only one side of a hooked property. Parent hook behavior is not automatically called; PHP provides syntax such as parent::$name::set($value) when the child intentionally delegates to it.
Hooks can be declared final to prevent a child from overriding that operation. A whole property can also be final, preventing redeclaration and hook changes.
Inheritance makes hooked properties harder to reason about because a simple read may execute child or parent behavior depending on the runtime class. Use final hooks where an invariant must remain fixed, and prefer composition when subclasses would need to rewrite most access behavior.
Interfaces Can Require Property Behavior
PHP 8.4 interfaces can declare hooked properties. A get requirement says implementing objects must provide readable behavior compatible with the declared property; a set requirement says they must provide writable behavior.
This can model a property-shaped contract, but it also ties the interface and every implementation to PHP 8.4. Method contracts remain more familiar and portable, especially in libraries supporting several PHP versions.
Choose an interface property when direct property access is intentionally part of the public model. Do not use one merely to avoid writing a method name.
Serialization And Debugging Are Not Uniform
Different inspection and serialization mechanisms treat hooks differently. json_encode(), var_export(), and get_object_vars() use get behavior in relevant cases, while var_dump(), native serialize(), array casting, and get_mangled_object_vars() use raw backing values. Custom __serialize(), __unserialize(), and JsonSerializable methods follow the logic you write.
That means a get hook that formats, redacts, or calculates data may produce different output between a JSON response and a debug dump. Test the actual serialization path used by the application. Do not assume every tool sees the same representation.
If an API response has strict requirements, an explicit response mapper or JsonSerializable implementation is often clearer than relying on incidental property enumeration.
Hooks Versus Traditional Methods
A hook is a good replacement for boilerplate methods when the public concept truly is a property. $person->fullName, $email->value, and $rectangle->area can read naturally.
Methods remain better when the operation takes parameters, represents a command, may perform I/O, has important failure modes, or changes several parts of the object. markPaid(), quoteFor($destination), and reload() communicate actions more honestly than property assignment.
Existing libraries should also consider backward compatibility. Replacing getName() with $name breaks callers even if the new syntax is attractive. Hooks are not a reason to churn a stable public API without benefit.
What You Should Be Able To Do
After this lesson, you should be able to create backed and virtual properties, use short and block hook syntax, validate or normalize writes, and derive read-only values. You should know that hooks require PHP 8.4 and cannot be combined with readonly properties.
You should also understand promoted-property input limits, indirect array modification, asymmetric set visibility, inheritance and final hooks, interface property requirements, and why serialization tools may expose hooked properties differently. Most importantly, you should be able to reject a hook when a method would make expensive work or side effects clearer.
Practice
Practice: Normalise A Hooked Property
Create a small PHP example using a set hook.
Task
Build a UserProfile class with an email property that:
- trims the assigned value
- lowercases it
- validates it as an email address
- stores only the normalised value
Use strict types. Keep the expected output in the PHP code block as printed lines or comments.
Check Your Work
Run cases for:
- assigning a valid email with spaces and uppercase letters
- assigning an invalid email
Afterward, explain why this hook is reasonable and what kind of work should not be hidden in a property hook.
Show solution
<?php
declare(strict_types=1);
final class UserProfile
{
public string $email {
set {
$email = strtolower(trim($value));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email address is not valid.');
}
$this->email = $email;
}
}
}
$profile = new UserProfile();
$profile->email = ' Ada@Example.COM ';
echo $profile->email . PHP_EOL;
try {
$profile->email = 'not-an-email';
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// ada@example.com
// Email address is not valid.
This hook is reasonable because it performs quick validation and normalisation directly related to the property. It should not send email, query a database, call an API, or trigger a business workflow.
Practice: Combine Backed And Virtual Properties
Create a product model with one normalized backed property and one calculated virtual property.
Task
Build a Product class with:
- a backed
nameproperty whose set hook trims input and rejects an empty name - promoted
pricePenniesandtaxRateproperties - a get-only virtual
priceWithTaxPenniesproperty - a get-only virtual
labelproperty combining the name and taxed price
Create one product and print its normalized name, taxed price, and label.
Check Your Work
Identify which properties consume storage, which are calculated on every read, and why assigning directly to either virtual get-only property should fail.
Show solution
The name stores normalized state, while both calculated properties derive their values from existing state.
<?php
declare(strict_types=1);
final class Product
{
public string $name {
set {
$name = trim($value);
if ($name === '') {
throw new InvalidArgumentException('Name cannot be empty.');
}
$this->name = $name;
}
}
public int $priceWithTaxPennies {
get => (int) round($this->pricePennies * (1 + $this->taxRate));
}
public string $label {
get => $this->name . ': ' . $this->priceWithTaxPennies . ' pennies';
}
public function __construct(
string $name,
public int $pricePennies,
public float $taxRate,
) {
$this->name = $name;
}
}
$product = new Product(' Keyboard ', 2000, 0.2);
echo $product->name . PHP_EOL;
echo $product->priceWithTaxPennies . PHP_EOL;
echo $product->label . PHP_EOL;
// Prints:
// Keyboard
// 2400
// Keyboard: 2400 pennies
name, pricePennies, and taxRate store values. The two virtual properties have only get hooks and therefore calculate their output without separate backing storage.