Reviewing Property Declaration In PHP
Properties are a foundation part of classes. They are declared with their visibility, and their name. Then, they might be augmented with a default value, a type (even several), and may be also options like static or readonly. With hierarchies and large families of classes, we shall start reviewing property declaration in PHP.
<?php class x { private $private; protected $protected; } class y extends x { // $protected is already declared above // protected $protected; } ?>
Contrary to methods and constants, properties cannot be marked final or abstract. The access to a property is handled by the visibility, which is a compulsory element of the property declaration. Either is it public
and protected
, and the property is available to all the children classes, and more; or it is private
and the property is only available to the current class. Also, don’t use var
anymore, please.
Class hierarchy and property definition
As shown above, a property may be defined in the parent class, and also be redefined in the children classes. As long as the property is not private
, it is valid. And then, it is also superfluous : once is sufficient.
To report this, Exakat has a Redefined Property rule, which spots classes which redefine an already defined property. Afte that, it is only a matter of removing the property.
The question arise : which one ?
Removing property definition
Indeed, which property definition should we keep? Let’s choose between the parent’s and the child’s : one can extends the reasoning to any higher level of duplication.
We’ll review the vertical hierarchy, where classes extend each other in direct line, and then we’ll cover the horizontal hierachy, where many classes extends the same class. We’ll finish with the hybrid hierarchy, and some extras.
Vertical hierarchy
In a vertical hierarchy, classes extend each other. The parent class is at the top, and the child class is at the bottom, just like a family tree. The proper level of definition for a property is that is should be as high as its usage.
Starting from the top class and going down to the children, the first class that holds a method which uses a property is the class that should define it. Above is useless, and below would lead to a missing definition.
<?php class x { protected $x; function foox() { echo $this->x; } } class y extends x { protected $y; function fooy() { echo $this->y . $this->y . $this->z; } } class z extends y { protected $z; function fooz() { echo $this->y . $this->x; } } class a extends z { protected $a; function fooa() { echo $this->a . $this->z; } } ?>
Let’s review the different illustrations here. They have the same name as the class, for easy reference, and each class has a foo*()
method, with *
being the name of the class.
$x
is used infoox()
, so this property is good.$y
is used infooy()
, so this one is good too.fooy()
also uses$x
, which comes from the parent : again, this is ok.- Then,
fooy()
uses$z
, which is defined below : that should not happens, since it is now a dependency between the classesy
andz
(see Property Used Below)
$z
is defined but not used locally :fooz()
uses$x
and$y
.$z
shouldn’t be defined there.$a
is used infooa()
, so this one is good.fooa()
also uses$z
, which comes from the direct parent :$z
should really be defined in thea
class.
Also, note that no class (besides a
) should use the $a
property, since it is only defined in the lowest class. Otherwise, this implies that the parent class has knowledge of the child class. And this would apply even with an abstract
class.
Horizontal hierarchy
The horizontal hierarchy is when one class is extended by many different classes. This is a very short vertical hierarchy, and the constraints are both the same and slightly different.
<?php class x { protected $x; function foox() { echo $this->x; } } class y extends x { protected $y; function fooy() { echo $this->y. $this->x; } } class z extends x { protected $z; function fooz() { echo $this->z. $this->x; } } class a extends x { protected $a; protected $y; function fooa() { echo $this->a. $this->y; } } ?>
We can apply the same rule as for the vertical hierarchy : a property must be used in the class where it is defined. So, properties in x
shall be used in x
(at least), and then, reused in other classes; properties in z
, y
,… shall only be used in those classes.
A collision occurs when the same property name appears in two (or more) different child classes. In the example above, $y
is used both in y
and a
. Several options are then available :
$y
offers the same features in both classes : then, as a common behavior, it should go up the tree and be defined inx
. A trait may also be considered.$y
has two different and distinct usages ina
andy
. Then, they should be named differently, to avoid confusion. Note that the confusion will be human, not PHP. Any clarification that coversa
andy
will have a hard time disambiguating between the$y
iny
and$y
ina
. It is well worth making them distinct.
Note that when a common property is detected in children, defining it in the parent class will make it available to the other children. If this is not intented, an extra layer of hierarchy may be introduced, or a trait.
Here is the above example, rewritten with a trait
<?php trait y { protected $y; } class x { protected $x; function foox() { echo $this->x; } } class y extends x { use y; function fooy() { echo $this->y. $this->x; } } class z extends x { protected $z; function fooz() { echo $this->z. $this->x; } } class a extends x { protected $a; use y; function fooa() { echo $this->a. $this->y; } } ?>
Here is the refactoring with a extra class. This extra class will make it easier to typehint on their specific capability, unlike the trait.
<?php class x { protected $x; function foox() { echo $this->x; } } class y2 extends x { protected $y; } class y extends y2 { use y; function fooy() { echo $this->y. $this->x; } } class z extends x { protected $z; function fooz() { echo $this->z. $this->x; } } class a extends y2 { protected $a; function fooa() { echo $this->a. $this->y; } } ?>
Hybrid hierarchy
When the hierarchy is wide and tall, then all drawbacks add up. The definition will be as low as possible in the hierarchy; yet, it will be highest usage that defines where it is defined. Also, some of the branches of the family might end up having a useless property, because it is not used in their part, but some distant cousins.
Trait approach might be the most pragmatic way to share this property.
<?php class x { protected $x; function foox() { echo $this->x; } } class y extends x { protected $y; function fooy() { echo $this->y. $this->x; } } class z extends x { protected $z; function fooz() { echo $this->y. $this->x; } } class a extends x { protected $a; protected $y; function fooa() { echo $this->a. $this->z; } } ?>
Abstract properties
Finally, let’s note that the some properties may differ one definition from the other. Indeed, PHP allows properties to be redefined as along as they are identical, in name and type, but not in default values. Those might differ.
<?php abstract class x { // default value, this can't happen protected int $id = 'none'; } class y extends x { // default value, this can't happen protected int $id = 'y'; } ?>
In such organisation, the parent class defines a property, just to ensure that its children have it defined too. The property is important, but the value is not : it might have to be overwritten in the underlying classes.
Based on the default value of the parent, it is possible to detect that a child hasn’t taken possession of that property, and react accordingly.
This behavior looks like an abstract
property : like for a method, the abstract property is defined in the parent class, yet MUST be defined in the child class to acquire a local default value. For methods, this is enforced by the PHP engine, while there is no such behavior for properties, nor for constants.
As there are no abstract properties nor constants in PHP so far, one solution is to avoid the hard-coded property definition, and to rely on the constructor to set the value, at execution time. This saves the second definition, although it artificially moves it to the execution realm. In fact, another static analysis rule, such as Could Set Default Property will complain. There is no way to satisfy everyone.
Tame those double definitions
In the end, a clean set of property definitions is quite simple to achieve :
- Create one property definition
- Make it as low as possible in the children definition, and as high as their first usage
- As long as abstract properties are not a PHP feature, it might be a PHP static analysis feature : reporting properties that should be defined in children classes, yet are defined in the parent class. This is a subject on its own, may be for the next time.
To see those issues emerge, one need quite a large application, with lots class hierarchies : both wide and large. These have been spotted mostly on frameworks and large libraries. It also happens on historic code, with many refactoring and different teams writing the code.
And that, is a good mission for exakat.