Data Transfer Objects In PHP 8

Data transfer objects (DTOs for short) are simple PHP classes which have one job: Store some data. Until recently the spatie/data-transfer-object package was our go-to when creating DTOs. This package recently became deprecated. There is a post explaining their decision so I wont go into the details but the short version is you don't need it anymore. With PHP 8 you can write effective DTOs without lots of boilerplate.

What Does It Look Like?

This DTO stores all the information required to crop an image:

readonly class Crop
{
    use Cloneable;

    public function __construct(
        public int $width,
        public int $height,
        public int $centreX,
        public int $centreY,
    )
    {
    }

    public function width(int $width): static
    {
        return $this->with(width: $width);
    }

    public function height(int $height): static
    {
        return $this->with(height: $height);
    }

    public function centreX(int $centreX): static
    {
        return $this->with(centreX: $centreX);
    }

    public function centreY(int $centreY): static
    {
        return $this->with(centreY: $centreY);
    }
}

Readonly

The class is readonly. This simply means that once constructed the properties can't be mutated. This is a PHP 8.2 feature. If you need to support PHP 8.1 you can mark each property as readonly instead:

class Crop
{
    use \Spatie\Cloneable\Cloneable;

    public function __construct(
        public readonly int $width,
        public readonly int $height,
        public readonly int $centreX,
        public readonly int $centreY,
    )
    {
    }

    // ...
}

Cloneable Trait

The Cloneable trait comes from the spatie/php-cloneable package. All it does is add a $this->with(...) method. Calling it returns a new instance, containing the same data except those passed to with. It's easier to show:

public function width(int $width): static
{
    return $this->with(width: $width);
}

This is equivalent to:

public function width(int $width): static
{
    return new self(
        width: $width,
        height: $this->height,
        centreX: $this->centreX,
        centreY: $this->centreY,
    );
}

This is a lot shorter but most importantly it doesn't repeat all the properties. Imagine if you had a large number of properties and then needed to rename/add/remove a property.

The Future

There is currently an RFC which, if it is accepted, will allow us to do this natively in PHP 8.3 rather than relying on reflection.

UPDATE: The RFC has been accepted!

The Result

Combing both these features we get a DTO which we can use like this:

$crop = new Crop(1024, 768, 512, 384);
$rectangle = $crop->width(250)->height(100);
$square = $crop->width(500)->height(500);

$crop !== $rectangle;
$crop !== $square;
$rectangle !== $square;

This is a pretty contrived example but hopefully it shows that we can have our cake and eat it the convenience of method chaining while keeping immutability.

Popular Reads

Subscribe

Keep up to date

Please provide your email address
Please provide your name
Please provide your name
No thanks