With the move to PHP 7.1, we can now document scalar method arguments, as well as return type declarations. This latter is of particular importance for end users, as they can then be assured that what they expect is what they will receive.
However, this poses a problem with fluent interfaces, particularly when inheritance of any type is involved, due to how PHP implements return type declarations: they are implemented as invariants, which leads to issues when implementations or extensions need to include the return type declaration.
As examples, given either of the following:
interface A
{
public function select(string $table) : self;
}
class B
{
private $address;
public function to(string $address) : self
{
$this->address = $address;
return $this;
}
}
Implementing A
or extending B
runs into issues, as we cannot use self
as a return type declaration when implementing either select()
or overriding to()
. For a single layer of extension, this is not a problem, as we can use parent
instead:
class C extends B implements A
{
private $table;
private $where;
public function select(string $table) : parent
{
$this->table = $table;
return $this;
}
public function to(string $address) : parent
{
$this->where = $address;
parent::to($address);
return $this;
}
}
IDE problems
While the above parses correctly, it poses a problem in IDEs, as they now assume that the return values are of type
A
andB
, respectively and notC
. As such, they will not allow expansion of methods inC
on the return value.
However, if we have another level of inheritance — e.g., class D
extending C
— neither self
nor parent
now work as return type declarations, and you instead need to provide the return type of the root declaration:
class D extends C
{
private $myOwnTable;
private $myCriteria;
public function select(string $table) : A
{
$this->myOwhTable = $table;
parent::select($table);
return $this;
}
public function to(string $address) : B
{
$this->myCritieria = $address;
parent::to($address);
return $this;
}
}
As noted above, this then means that IDEs now think the return value is of another type entirely, meaning they cannot hint on the methods from the actual class.
Some possible solutions:
- Use annotations instead:
@return self
or@return static
or@return $this
. These, however, mean that the contract is not enforced at the engine level. I largely feel this is a non-starter, as it goes against our reasons for moving to 7.1 in the first place. - Don’t worry about it, and just use the root declaration. This indicates an “is-a” relationship, and enforces the idea that a class operates as a general type, promoting the idea of re-use of generic types, and not concrete types. This would be the simplest way and pose the least amount of backwards compatibility breakage, but could pose issues for consumers in components such as zend-form that have multiple levels of inheritance when using IDEs.
- Deprecate usage of fluent interfaces except when used for implementing immutability (e.g., PSR-7’s
with*()
methods), where return type declarations are indicating the interface type returned, and not the instance type. Most fluent interfaces within the framework would then instead returnvoid
. This would pose the most BC breakage, but forward-proof the APIs the most.
Please let us know in the comments what solution you prefer, and why.