Software development often presents dilemmas, such as how to handle situations when a getter has nothing to return. In this article, we'll explore three strategies for implementing getters in PHP, which affect the structure and readability of code, each with its own specific advantages and disadvantages. Let's take a closer look.
Universal Getter with a Parameter
The first solution, used in Nette, is to create a single getter method that
can either return null
or throw an exception if the value is not
available, depending on a boolean parameter. Here is an example of what the
method might look like:
public function getFoo(bool $need = true): ?Foo
{
if (!$this->foo && $need) {
throw new Exception("Foo not available");
}
return $this->foo;
}
The main advantage of this approach is that it eliminates the need to have
several versions of the getter for different use cases. A former disadvantage
was the poor readability of user code using boolean parameters, but this has
been resolved with the introduction of named parameters, allowing you to write
getFoo(need: false)
.
However, this approach may cause complications in static analysis, as the
signature implies that getFoo()
can return null
under
any circumstances. Tools like PHPStan allow explicit documentation of method
behavior through special annotations, improving code understanding and its
correct analysis:
/** @return ($need is true ? Foo : ?Foo) */
public function getFoo(bool $need = true): ?Foo
{
}
This annotation clearly defines what return types the method
getFoo()
can generate depending on the value of the parameter
$need
. However, for instance, PhpStorm does not understand it.
Pair of Methods:
hasFoo()
and getFoo()
Another option is to divide the responsibility into two methods:
hasFoo()
to verify the existence of the value and
getFoo()
to retrieve it. This approach enhances code clarity and is
intuitively understandable.
public function hasFoo(): bool
{
return (bool) $this->foo;
}
public function getFoo(): Foo
{
return $this->foo ?? throw new Exception("Foo not available");
}
The main problem is redundancy, especially in cases where the availability
check itself is a complex process. If hasFoo()
performs complex
operations to verify if the value is available, and then this value is retrieved
again using getFoo()
, these operations are repeated.
Hypothetically, the state of the object or data might change between the calls
to hasFoo()
and getFoo()
, leading to inconsistencies.
From a user's perspective, this approach may be less convenient as it forces
calling a pair of methods with repeating parameters. It also prevents the use of
the null-coalescing operator.
The advantage is that some static analysis tools allow defining a rule that
after a successful call to hasFoo()
, no exception will be thrown in
getFoo()
.
Methods getFoo()
and
getFooOrNull()
The third strategy is to split the functionality into two methods:
getFoo()
to throw an exception if the value does not exist, and
getFooOrNull()
to return null
. This approach minimizes
redundancy and simplifies logic.
public function getFoo(): Foo
{
return $this->getFooOrNull() ?? throw new Exception("Foo not available");
}
public function getFooOrNull(): ?Foo
{
return $this->foo;
}
An alternative could be a pair getFoo()
and
getFooIfExists()
, but in this case, it might not be entirely
intuitive to understand which method throws an exception and which returns
null
. A slightly more concise pair would be
getFooOrThrow()
and getFoo()
. Another possibility is
getFoo()
and tryGetFoo()
.
Each of these approaches to implementing getters in PHP has its place depending on the specific needs of the project and the preferences of the development team. When choosing a suitable strategy, it's important to consider the impact on readability, maintenance, and performance of the application. The choice should reflect an effort to make the code as understandable and efficient as possible.
Leave a comment