The following article is a short snippet from the "Code Techniques" chapter of my book "Consuming APIs In Laravel".
The book teaches you how to write powerful and robust code to build API integrations in your Laravel apps. It's packed with over 440+ pages of actionable advice, real-life code examples, and tips and tricks.
In the book, I teach you how to write code that can help you sleep easy at night having confidence that your API integration works!
If you enjoy this content, you might want to check out the rest of the book. You can download a FREE sample from the "Webhooks" section that covers "Building Webhook Routes" and "Webhook Security".
๐ Make sure to use the discount code CAIL20 to get 20% off!
Readonly Classes and Properties in PHP
A powerful feature introduced in PHP 8.1 is readonly properties. It allows us to prevent properties from being changed after they have been set and can give us confidence we aren't accidentally changing a property we shouldn't.
PHP 8.2 then introduced the ability to make entire classes readonly. This can be particularly useful for data transfer objects (DTOs) because it allows us to prevent all the properties from being changed after the DTO has been instantiated.
Let's look at an example to understand the benefits of using readonly classes and properties:
1final class Repo 2{ 3 public function __construct( 4 public int $id, 5 public string $name, 6 public string $fullName, 7 public bool $isPrivate, 8 public string $description, 9 public CarbonInterface $createdAt,10 ) {11 //12 }13}
At the moment, our DTO is mutable, meaning we can change the values of any of the properties after the DTO has been instantiated. For example, we could do the following:
1$repo = new Repo( 2 id: 123, 3 name: 'short-url', 4 fullName: 'ash-jc-allen/short-url', 5 isPrivate: false, 6 description: 'A URL shortener', 7 createdAt: Carbon::now(), 8); 9 10$repo->name = 'short-url-2';
The code in the example would allow us to update the name
property of the DTO from "short-url"
to "short-url-2"
. This is because the name
property is currently public and can be updated from inside or outside of the class. Depending on how you're using the DTOs, you may not want to allow this. Prior to PHP 8.1, to prevent the property from being changed, we would need to add a private property and a public getter method to retrieve the value of the property:
1final class Repo 2{ 3 public function __construct( 4 private int $id, 5 private string $name, 6 private string $fullName, 7 private bool $isPrivate, 8 private string $description, 9 private CarbonInterface $createdAt,10 ) {11 //12 }13 14 public function getName(): string15 {16 return $this->name;17 }18 19 // Other getter methods here...20}
Changing each of the properties to be private would prevent them from being changed after the class is instantiated, whether it be accidental or intentional. However, this approach has several drawbacks:
- It requires us to add a getter method for each property we want to access from outside the class.
- It adds a lot of boilerplate code to the class that adds cognitive load when reading it.
To solve these issues, we could change this DTO to use readonly properties instead:
1final class Repo 2{ 3 public function __construct( 4 public readonly int $id, 5 public readonly string $name, 6 public readonly string $fullName, 7 public readonly bool $isPrivate, 8 public readonly string $description, 9 public readonly CarbonInterface $createdAt,10 ) {11 //12 }13}
As a result of using the readonly
keyword, we've been able to remove the getter methods from the class. We've also been able to change the visibility of the properties to public
because we can be confident that the fields cannot be updated after the class has been instantiated.
If any code was written, whether intentional or accidental, to update the value of any of the properties, PHP would throw an error. For example, if we tried to update the name
property of the DTO, PHP would throw the following error:
1Cannot modify readonly property App\DataTransferObjects\Repo::$name
To take this further, if you are confident that no property in the DTO should ever be changed after the class has been instantiated, you can make the entire class readonly:
1final readonly class Repo 2{ 3 public function __construct( 4 public int $id, 5 public string $name, 6 public string $fullName, 7 public bool $isPrivate, 8 public string $description, 9 public CarbonInterface $createdAt,10 ) {11 //12 }13}
As a result of making the class readonly, PHP will throw an error if our code attempts to change any of the properties' values.
By default, I like to make as many of my classes and properties readonly as possible. Although it may sometimes seem overkill and unneeded, I like the visual cue it gives me when reading my code โ especially when I'm reading code I wrote several months ago. It clearly indicates that a given class or property isn't updated anywhere in the codebase after it's instantiated. If the code needs to be updated to allow a property to be changed, I can remove the readonly
keyword.
Enjoyed This Snippet?
If you enjoyed this snippet, you might want to check out the rest of the "Consuming APIs in Laravel" book.
๐ Make sure to use the discount code CAIL20 to get 20% off!