Photo by Peter Masełkowski on Unsplash
I've come a long way with PHP and using of Getters, Setters and DocBlocks was a no-brainer for me for many years. They somehow belonged to PHP as if they were built-in features. Often enough, they were (and still are) simply part of the frameworks we used. Yet with PHP 8 released, it's time to rethink some good old habits and reduce boilerplate code with PHP 8.
Looking back
The below code sample shows how I most likely would have programmed a simple DTO (Data Transfer Object) with two properties in the times of PHP 5.
class SampleDto
{
/**
* @var int
*/
private $property1;
/**
* @var string
*/
private $property2 = 'test';
/**
* @return int
*/
public function getProperty1()
{
return $this->property1;
}
/**
* @param int $property1
* @return SampleDto
* @throws Exception
*/
public function setProperty1($property1)
{
if (!is_int($property1)) {
throw new \Exception("property1 must be of type integer");
}
$this->property1 = $property1;
return $this;
}
/**
* @return string
*/
public function getProperty2()
{
return $this->property2;
}
/**
* @param string $property2
* @return SampleDto
* @throws Exception
*/
public function setProperty2($property2)
{
if (!is_string($property2)) {
throw new \Exception("property2 must be of type string");
}
$this->property2 = $property2;
return $this;
}
/**
* SampleDto constructor.
* @param int $property1
* @param string $property2
* @throws Exception
*/
public function __construct($property1, $property2)
{
$this
->setProperty1($property1)
->setProperty2($property2);
}
}
Wow, that's quite a bit of code for a simple DTO! Why was all of that stuff necessary? Mostly because of PHP's Dynamic Typing (or Type Juggeling, as it is called on the official php.net website).
PHP 5 did not offer any useful type hinting functionality. If we wanted to ensure that properties were initialized properly, using Setter-Methods with some checks was a common and good practice to overcome the shortcomings of PHP when it comes to type safety.
Even with dynamic typing, knowing which type a variable was supposed to have helped a lot during development. Since PHP didn't support this, we had to add it ourselves - DocBlocks were conceived and soon, although never been an official language construct, accepted and supported widely amongst the IDEs.
The lack of type hinting required a lot of code just to ensure that attributes were used correctly.
Surely it was possible to write PHP code without caring about this kind of things at all, but just because something is possible, it doesn't mean it's a good idea to do so.
Less is more
Looking again at the example above, you can see that a lot of lines are used up by the DocBlocks already. As mentioned above, they had good reasons to exist. They helped us overcome the problems of dynamic typing. With native Type Hinting support, is there any reason left to still use them? Is there a way to reduce some boilerplate code?
Interestingly, this question is still worth a little debate here and there, were developers defend the usage of DocBlocks like this:
/**
* @param $property1
*/
public function setProperty1($property1) { // ... }
Often auto-generated by the IDE, this little fella does not serve any practical use, as it does not add any valuable information. It's a waste of space and unnecessarily increases the cognitive load of the developers.
The only thing worse than unnecessary DocBlocks are wrong DocBlocks:
/**
* @var int
*/
private $property1;
/**
* @param string $property1
*/
public function setProperty1($property1) {
$this->property1 = $property1;
}
At some point, the "type" (or let's say the purpose) of $property1
has been changed, yet we forgot to update the DocBlocks of the set function. Not a big deal? Well, the next developer who comes across this code now needs to dig deeper into to find out what type $property1
should actually be off. Or we just ignore it right away.
Avoid DocBlocks, if you can
In my opinion, comments should be avoided if possible (and I'm not the only one). Comments are not part of the executed code. Mistakes made in comments will not cause things to blow up. That's why often enough comments are not updated during refactoring and, in the end, are causing more harm than benefits.
DocBlocks still have valid use cases though. You can use it for declaring the type of items in an array. Marking methods as deprecated is pretty handy as well. Also, some ORMs require parsing the DocBlocks for annotations to work. In this case, though, mistakes made in the annotations would cause noticeable effects aka as bugs and are hopefully caught by automated tests.
Only add DocBlocks if they add information to your code which can not be made available otherwise. Skip the redundant parts. If they don't add value, remove them completely.
An example of a useful DocBlock is
class SampleApi
{
/**
* @deprecated will be removed with version 2
*/
public function createSomething(int $param1): self
{
// ....
}
}
The types of the function parameter or the return value require no further documentation. Yet the information that the function is deprecated is of great use, so we can add a DocBlock with only that information, and leave out the unnecessary stuff.
Finally: PHP 8 attributes
I am aware that many PHP frameworks still require annotations in DocBlocks, but that should not be the benchmark. Even the most popular framework in PHP makes heavy use of anti-patterns, so that should not keep us from challenging them.
If your framework (or ORM or whatever package) is relying on DocBlocks to work, there is still hope: with PHP 8, attributes were introduced. Although I admit it will take a bit until I get used to the syntax, attributes will greatly help us get rid of DocBlocks and thus reduce boilerplate code.
And it seems it already started: as the highly respected Tomas Vostruba noted in one of his recent posts, Symfony 5.2 introduced the #[Route]
attribute to replace the @Route
annotations:
/**
* @Route(path="/archive", name="blog_archive")
*/
public function blogArchive(): Response {}
// will become
#[Route(path: '/archive', name: 'blog_archive')]
public function blogArchive(): Response {}
So finally, the valuable information will no longer be stored in comments, but in native PHP code. That's the way to go!
More cleanup
Another nice benefit of type hinting is that PHP 7 throws errors if the wrong type was used. That's why we can remove manual type checking in the Setter methods completely, which reduces the amount of boilerplate code even more:
class SampleDto
{
private int $property1;
private string $property2 = 'test';
public function getProperty1(): int
{
return $this->property1;
}
public function setProperty1(int $property1): self
{
$this->property1 = $property1;
return $this;
}
public function getProperty2(): string
{
return $this->property2;
}
public function setProperty2(string $property2): self
{
$this->property2 = $property2;
return $this;
}
public function __construct($property1, $property2)
{
$this
->setProperty1($property1)
->setProperty2($property2);
}
}
That's much shorter already! At this point, you can surely argue if it is necessary to allow the initialization of the properties via the constructor. If you don't need it, it will save you some lines as well.
Much more space is eaten up by the Getter and Setter methods though. With PHP 7.4, we have property type hinting, so what do we actually still need them for? They don't serve any real, additional purpose anymore.
Ok then, wait for a second - what if we just declare these properties as public in the first place then?
It took me a while to overcome my habits, so the answer actually kind of surprised me as well:
We don't need Getters and Setters for public properties anymore.
Please don't get me wrong at this point: I don't say we don't need Getters and Setters anymore at all. Same as for the DocBlocks, we just should use them only if they are really necessary.
In my example, I used a DTO. These are simple objects which only serve the purpose of moving data around. We can actually do this:
declare(strict_types=1);
class SampleDto
{
public int $property1;
public string $property2;
public function __construct(int $property1, string $property2 = 'test')
{
$this->property1 = $property1;
$this->property2 = $property2;
}
}
Now THAT'S a bit crazy, isn't it? Well, actually not! We still ensure type safety. I see no benefit of using Getters and Setters here at all. It just feels strange to me, but I can't help it:
// why not just use
$myDto->property1 = 5;
// instead of
$myDto->setProperty1(5);
Again: it's all about ways to reduce boilerplate code. If we don't get any benefits from, why use it?
Introducting PHP 8
It gets even cooler: with PHP 8, we can now use Constructor property promotion to reduce the code even more. The next example is, in my opinion, reduced as much as possible, without losing anything:
declare(strict_types=1);
class SampleDto
{
public function __construct(
public int $attribute1,
public string $attribute2 = 'test'
) {}
}
Behold the result for a moment: we reduced the number of lines necessary for this DTO from 75 to 9! And we did not sacrifice anything! In fact, our code is more robust than before, since we now only use native PHP features and nothing else.
Please note though: I'm not opting for making all properties public! Private and protected properties are still necessary. An example could be a Repository which gets the database connection object injected via the constructor, and making it impossible to change it afterwards:
class SomeRepository
{
public function __construct(
private DatabaseConnection $dbConnection,
public string $attribute
) {}
This example is pretty made up, but I'm sure you'll get the idea! However:
If you have a private property and add Getter and Setter methods for it, there is no objection against making it public in the first place anymore.
Contra
Below I added some contra-arguments which I encountered recently.
1. Casting and formatting of properties
A common use case for Getters is to receive data in a different way than it is stored in the object, e.g. formatting dates or strings in a certain way. This is not what Getters should be used for in my opinion. Getters should return the data unmodified. Add dedicated methods like toDate()
or toFormattedOutput()
or whatever you like if you need it.
2. Immutable Objects
You probably came across Immutable Object already. The PSR-7 HTTP message interface is a common example for it: once created, you can't alter an instance. The only way is to create a modified copy out of it.
Since there is no native support for this in PHP yet, achieving this goal requires some more manual work. Decide for yourself if it is worth the effort. I personally think it is not, at least for the average PHP script which is stateless between the requests.
With native support, however, I think it would be a beneficial addition though. In yet another brilliant article from Larry Garfield, he explores the topic of Object properties and immutability in great depth. I wouldn't be surprised if this would, in some way, become part of PHP soon.
Conclusion
PHP has come a long way. It's still changing a lot, and so should we, the developers. Just because we used certain patterns for years, even if they served us well, it doesn't mean we shouldn't let them go one day.
In my earlier years as a developer, I always fought hard for adding DocBlocks and using Getters and Setters everywhere. At that time I think it made sense. With the latest changes of PHP though, I'm now convinced that we can improve our code a lot by getting rid of them.
PHP matures, so should we.
DocBlocks still can add valuable information to our code, yet we should only add them where we can't add this information with language features.
The same is true for Getters and Setters. There might be use cases for still using them together with Constructor injection, but I want to challenge you to double think about it.
With PHP 8, we can reduce boilerplate code by a lot. This will make our lives much easier.
What's your view on this? Is it time to ditch Getters, Setters and DocBlocks, or are there good reasons that I did not consider?
Top comments (0)