No attributes should be defined with type in PHP
In PHP we have the possibility of adding type to the attributes of a class. This is a great feature of PHP and can be used from PHP 7.4. However, it is important to know how to use it to avoid some errors that may come with this feature.
Defining an attribute with a type is as simple as placing the type before the variable name like this:
protected string $name;
At this point, it is very important to ask yourself if this data will ever be null or will always be a string, since if you try to access it without being initialized, you will get an error like the following:
$name must not be accessed before initialization
This is a very common error and it is very easy to obtain. From now on, we will use variations of the following example to make our point. Notice how this error could be caused.
class Request
{
protected string $body;
public function getBody(): string
{
return $this->body;
}
}
$request = new Request();
$request->getBody(); // accessing the attribute without initializing it
There is no doubt that this makes our code much more prone to errors! When we use types in PHP or any other programming language, we always have to make sure to initialize the variables! This is the classic problem of forgetting to handle null references exposed by Tony Hoare in The billion Dollar Mistake.
Required attributes
One way to avoid this error is to think if the attribute is required. In this case, the solution would be to receive it in the constructor like this:
class Request
{
protected string $body;
public function __construct(string $body)
{
$this->body = $body;
}
public function getBody(): string
{
return $this->body;
}
}
Since the data is required in the constructor as a string, the type check would be done from the creation of the object itself, so it is no longer the responsibility of this class to ensure that the body attribute was initialized. With this, we would no longer get an error when accessing said attribute since it is always initialized in the constructor.
Non-required attributes
When an attribute is not required, it is always recommended to initialize the attribute to a default value, whether it is null, zero, empty, etc. Let's see how our example would look like with this premise.
class Request
{
protected ?string $body = null;
public function getBody(): ?string
{
return $this->body;
}
public function hasBody(): bool
{
return (bool) $this->body;
}
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
}
If we do not initialize the attribute to a value, we would have to verify each time if the attribute has been initialized and this would add extra logic by adding more responsibility than necessary. Let's see how it would be.
class Request
{
protected ?string $body;
public function getBody(): ?string
{
return $this->body ?? null;
}
public function hasBody(): bool
{
return (bool) ($this->body ?? false);
}
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
}
Every time we want to use said attribute, we would have to use the null coalescing operator.
$this->body ?? null
This makes the code much more fragile, since it depends on the programmer to add said check every time. If you use PHP with a version prior to 7, the code would be even more scandalous.
isset($this->body) ? $this->body : null
Attributes without type
Ultimately, you can always use attributes without type. Even this way, the code is much less fragile since by default the attribute will be null and it can only be modified with a setter function that verifies the data type.
class Request
{
protected $body;
public function getBody(): ?string
{
return $this->body;
}
public function hasBody(): bool
{
return (bool) $this->body;
}
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
}
In conclusion, it is recommended to use attributes with type whenever they are initialized or their initialization is ensured in the constructor function. Otherwise, the type can be verified with a setter function whenever it is not required, and we should not use variables with type.