Constant Constants. Finally! (On the inconstancy of constants)

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.

This entry was posted in PHP and tagged , , , , . Bookmark the permalink.

1 Response to Constant Constants. Finally! (On the inconstancy of constants)

  1. Pingback: Symfony Station Communique - 26 November 2021. A Look at Symfony and PHP News. - The web development company

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s