One of the many new features of PHP 8.1 is the ability to declare class constants as final
, so that they can no longer be overridden in child classes. The same applies when constants are defined as final in an abstract classes, or interface; they can’t be overridden by classes extending that abstract or implementing that interface. So class and interface constants can now truly become constant.
You could be forgiven for believing that a constant will always retain the same value throughout, it’s in the name, right? And technically that’s correct. But it all comes down to context, to the class (or interface) where that constant is defined, and its visibility; and when it comes to class inheritance, all bets are off. The value of a constant can be defined at one level within the inheritance tree; but overridden at another level. Technically, it’s a different constant; but having constants with the same name yet different values within a class inheritance tree can certainly create confusion, and potentially lead to awkward bugs. The PHP documentation does note that this can be the case; but perhaps it really should be a warning instead.
Let’s look at a simple example. Prior to PHP 8.1 we might have created an interface and classes like:
interface Politician {
public const STATUS_HONESTY = true;
public function honesty();
}
class Candidate implements Politician {
public function honesty() {
echo 'I am ',
static::class === self::class ? 'a ' : 'now a ',
static::class, ' and I am ',
$this::STATUS_HONESTY ? 'honest' : 'a liar', PHP_EOL;
}
public function getElected(): Politician {
return new MemberOfParliament();
}
}
class MemberOfParliament extends Candidate {
public const STATUS_HONESTY = false;
}
As you can see, the MemberOfParliament
class overrides the STATUS_HONESTY
constant defined in the Politician
interface, and PHP allows this to happen. This is because we actually have two different constants: Politician::STATUS_HONESTY
and MemberOfParliament::STATUS_HONESTY
; but when referencing those constants from inside the code itself we need to be very careful about whether we use self::STATUS_HONESTY
, static::STATUS_HONESTY
or $this::STATUS_HONESTY
. In this case, static::STATUS_HONESTY
and $this::STATUS_HONESTY
reference the value of STATUS_HONESTY
constant for the object instance – Politician
, inherited from the interface definition, or MemberOfParliament
– while self::STATUS_HONESTY
will always reference Politician::STATUS_HONESTY
.
I’m cheating a little bit here – I should really have created an abstract class with the honesty()
method implementing Politician
, and both Candidate
and MemberOfParliament
should have extended that – but bear with me for the moment.
When we run the following code, and elect a Candidate
, we get a new MemberOfParliament
returned:
$politician = new Candidate();
$politician->honesty();
$politician = $politician->getElected();
$politician->honesty();
which gives us:
I am a Candidate and I am honest
I am now a MemberOfParliament and I am a liar
This is pretty much what we expect; but not really what we might want. We should expect our politicians to be honest, even after they’ve been elected, and not just when they’re on the campaign trail; our class and interface constants not to be overridden in child classes, but to remain constant. From PHP 8.1, we can enforce this, at least for class constants… sadly, not for our politicians.
Just by declaring STATUS_HONESTY
as final
in our Politician
interface:
interface Politician {
final public const STATUS_HONESTY = true;
public function honesty();
}
we now get a Fatal Error
if the code tries to override the value of STATUS_HONESTY
in any classes that implement (or extend an implementation) of Politician
:
Fatal error: MemberOfParliament::STATUS_HONESTY cannot override final constant Politician::STATUS_HONESTY
Of course, we still have to deal with a Fatal Error at runtime if we allow this in our code; but we should see this in our unit tests, or static analysis tools like Psalm and PHPStan should be able to detect this in our CI pipeline, so that we can fix our code before it is deployed.
There is another quirk to this change though. We can apply final
to public
and protected
constants; but applying final
to a private
constant will always result in a Fatal Error
interface Politician {
final private const STATUS_HONESTY = true;
}
results in
Fatal error: Private constant Politician::STATUS_HONESTY cannot be final as it is not visible to other classes
By definition, a private constant is only accessible in the class where it is defined, so there is no real point in defining it as final.
Again, unit tests and static analysis tools should be able to identify this before we deploy the code.
Note that these Fatal Errors are triggered when the file that contains the class is (auto)loaded, when PHP parses the class definition, not simply when the class is instantiated. Normally the file will be loaded when the class is instantiated; but if you’re using any preloading, these errors will trigger then rather than during actual code execution.
I asked you to bear with me earlier about avoiding an abstract class between the Politician
interface and the Candidate
and MemberOfParliament
concrete classes. That’s because there is also another change to overriding interface constants in our classes worth noting here. Prior to PHP 8.1, we could not override a constant defined in an interface in any class that implemented that interface. The following code would throw a Fatal Exception in earlier versions of PHP:
interface Politician {
public const STATUS_HONESTY = false;
public function honesty();
}
class Candidate implements Politician {
protected const STATUS_HONESTY = true;
public function honesty() {
...
}
public function getElected(): Politician {
return new MemberOfParliament();
}
}
giving
Fatal error: Cannot inherit previously-inherited or override constant STATUS_HONESTY from interface Politician
But from PHP 8.1, it is permitted to override an interface constant in an class that implements that interface, as long as the constant isn’t defined as final in our interface, so the above code would give us:
I am a Candidate and I am honest
I am now a MemberOfParliament and I am a liar
Note that this restriction only applied to classes that directly implemented the interface, not to classes that extended from them.
interface Politician {
protected const STATUS_HONESTY = true;
public function honesty();
}
class Candidate implements Politician {
public function honesty() {
...
}
public function getElected(): Politician {
return new MemberOfParliament();
}
}
class MemberOfParliament extends Candidate {
protected const STATUS_HONESTY = false;
}
So overriding STATUS_HONESTY
in the Candidate
class would throw that Fatal Error, but overriding it in the MemberOfParliament
that extends Candidate
was allowed. Worth remembering if you use abstract classes that implement your interfaces, and which you then extend for your concrete classes.
Of course, now that PHP 8.1 allows overriding of interface constants in classes that implement that interface, it’s no longer a concern if we’re running the newest release of PHP.
While there are certainly some valid use-cases for overriding constant values in a class hierarchy; more often than not we do want class constants that we define to be constant. And I for one am very happy that PHP 8.1 has finally provided a means of ensuring that constant values cannot be accidentally overridden.
Pingback: Symfony Station Communique - 26 November 2021. A Look at Symfony and PHP News. - The web development company