November 18, 2024

PHP 8.4 Property Hooks and Data Objects

Example of how property hooks can simplify the syntax of data objects, replacing native arrays when you need to make sure that your units of encapsulated data is type-safe and validated.

We’re only 3 days away from the first official release of PHP 8.4! This new version will implement a new feature named property hooks and I have to say that I have not been this excited about a new PHP release in a while.

What are property hooks?

Unless you code in languages like C#, Kotlin or Swift, chances are that you’ve never heard of property hooks before. In a nutshell, they are getters and setters defined directly on the property itself.

So instead of defining your class properties as private and creating getter and setter methods, you can now have your properties as public but still have logic attached to them when they are accessed or mutated.

Let’s look at an example

// Without PHP 8.4 Property Hooks
class Product
{
	private string $product_name;

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

	public function getProductName()
	{
		return $this->product_name;
	}

	public function setProductName(string $product_name)
	{
		$this->product_name = $product_name;
	}
}

// With PHP 8.4 Property Hooks
class Product
{
	public function __construct(
		public string $product_name {
			get => $this->product_name;
			set => $value;
		}
	) {}
}

The property hooks can be extended to include additional logic just like a normal method in a class, like get { return $this->product_name; } and set (string $product_name) { $this->product_name = $product_name; }

What’s the big deal?

At this point, you might be a bit skeptical about why this is such a big thing, aren’t they just plain old getters and setters? Well, hear me out.

As PHP developers, we haven’t got a reputation for being fussy about type safety of our data. But, I strongly feel that PHP has matured massively since PHP 8 was first released. I see that developers like myself who have done mostly PHP in their careers now being more specific about types in their code.

I believe that libraries like PHPStan and Psalm that statically analyze your code, together with more third-party libraries adding proper PHP Docs to their code, giving you immediate feedback in your code editor of any errors, really push the whole PHP community towards a better future of more predictable, type-safe and bug-free code.

Encapsulated units of data

What makes me the most excited about property hooks is how they make it easier to make type-safe and validated encapsulated units of data. Currently, we still do not have type-safe arrays in PHP, but with the introduction of property hooks, the syntax for creating data objects has become so much simpler.

I think this will create many opportunities and use cases where data objects would be a better option than using a plain PHP array. Data objects also have the additional advantage of type safety and other options like logging, validation and permission checking for secret properties.

Let’s look at an example:

//
// 1. using arrays to encapsulate data
//
// is 'name' a string? how long can it be? any limitations?
// is 'quantity' an int? string? does it take negative numbers?
// is 'price' an int? string? float?
$productData = [
    'name'     => 'Product A',
    'quantity' => 5,
    'price'    => '200.30'
];
// additional logic needed to verify that data is valid
validator($productData, [
    'name'     => ['required', 'string', 'max:255'],
    'quantity' => ['required', 'integer'],
    'price'    => ['required', 'string']
])->validate();

//
// 2. using class with property hooks to encapsulate data
//
class ProductData
{
    public function __construct(
        public string $name {
            set {
                if (strlen($value) > 255) {
                    $msg = 'Property $name exceeds 255 characters';
                    throw new \InvalidArgumentException($msg);
                }
                $this->name = $value;
            }
        },
        public int $quantity,
        public string $price
    ) {}
}

This is just an example but right off the bat, you see that we get the benefit of knowing what type of data all the properties are. In addition, validation can also be added to make sure that the data is valid for the purpose we’re using it for.

If we use plain arrays to do the same, we’d have to decouple the validation logic from the actual data, making it difficult to know if the data is indeed valid when passing it around in the application.

Passing data throughout layers

If you think of your application as a composition of multiple layers, you see that during an execution of a typical web application, data flows from the UI layer to the domain layer, down to the infrastructure layer and bubbles the same way up in reverse order.

When passing data throughout these layers, it’s more practical and safe to pass it as one unit, than having logic related to the data spread out in multiple layers.

Let’s think of a practical example where we use a data object. Let’s say that you get an update product request to your application. You would go ahead and transform the incoming request body data into one unit of data, a data object. This object is type-safe by nature and validated when it’s instantiated or modified. This way, you’ll always have the validation logic related to the data transferred together with the data itself. This assures that the data is always valid for its given purpose.

When using plain arrays you have to validate the incoming request body somewhere separate from the data itself, usually in a controller or form request object. This validation logic validates the data once but doesn’t take into account any modifications that might happen to the data when it moves from layer to layer. It becomes increasingly difficult to know and determine if the data is indeed valid in its current state at a given point in time.

Plain PHP arrays certainly still have its place and I don’t think that you should just go and make every array a data object. However, for data that is grouped and represents one logical unit, it makes sense to make it into one encapsulated unit of data.

Here’s an example of how fetched data from various sources can be represented as a request body data object that is then sent to an external API.

class ProductsRequestBody
{
    public function __construct(
		public string $name,
		public int $quantity,
		public array $taxonomy {
			set {
				if ($value === []
                || $value !== array_filter($value, 'is_string')) {
                    $msg = 'taxonomy must be an array of strings';
					throw new \InvalidArgumentException($msg);
				}
				$this->taxonomy = $value;
			}
		}
	) {}
}
$requestBody = new ProductsRequestBody(
    name: 'Product A', quantity: 5, taxonomy: ['cars']
);
Http::post('https://external-api.com/products', (array) $requestBody);

Data Objects and Eloquent

As you can tell, I’m a fan of the Laravel framework and also like Eloquent. However, I think that how Eloquent is structured, managing repository logic, business logic and infrastructure data in one huge chunk can get bloated and complicated.

In my future projects with PHP 8.4 moving forward, I aim to abstract the data part of my Eloquent models and manage the data into separate data objects instead. This will keep my data and repository logic separate, enabling me to pass the data around without also passing around the whole Eloquent repository logic with it. I can also keep the Eloquent data, which will be stored in the database, as a unit that I know is validated and matches my database schema.

On a competence day at work, I put together some logic of how I imagine that it will look. I pushed it one bit further and also added PHP 8 Attributes that read an array of validation rules that are passed on to Laravel’s amazing validator class.

class ProductsData extends DataObject
{
    #[ValidIf(['gt:0'])]
    public int $id {
        set => $this->validateProperty('id', $value);
    }

    #[ValidIf(['min:1', 'max:255'])]
    public string $name {
        set => $this->validateProperty('name', $value);
    }

    #[ValidIf(['min:1'])]
    public string $price {
        set => $this->validateProperty('price', $value);
    }

    #[ValidIf(['in:visible,hidden'])]
    public string $status {
        set => $this->validateProperty('status', $value);
    }

    // no validation needed
    public int $quantity {
        set => $this->validateProperty('quantity', $value);
    }

    #[ValidIf(['nullable', 'date'])]
    public ?Carbon $created_at {
        set => $this->validateProperty('created_at', $value);
    }

    #[ValidIf(['nullable', 'date'])]
    public ?Carbon $updated_at {
        set => $this->validateProperty('updated_at', $value);
    }

    /**
     * @return class-string
     */
    protected function getEloquentClassName(): string
    {
        return Products::class;
    }
}

In this data object, not all properties are required, so they’re moved out of the constructor function and declared as properties on the class. This way, it’s possible to update only the database columns that you want.

This class extends a DataObject base class that I wrote to include convenience methods like ->toArray(), ->toEloquent(), ->onEloquent() to more easily interact with Eloquent from your data object.

If you’re interested in looking into the code behind the data object and attribute validation logic, then it’s located [here]

That’s it for this time, I hope that this will be useful for someone out there wondering about the new property hooks feature that is coming in PHP 8.4 and some ideas on how it can be implemented and used in your apps.

Until next time, have a good one!

Oliver Lundquist

Born in 🇸🇪, resident in 🇯🇵 for 15+ yrs, husband and father of a daughter and son, web developer since 2009.

Read more about me
• mail@oliverlundquist.com• Instagram (@olibalundo)