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.