I recently saw this tweet by Jess Archer, which showed some Trait behavior that might not
seem logical at first glance. I thought it could be fun to explain why this happens.
This behaviour surprised me!
— Jess Archer (@jessarchercodes) August 24, 2021
(Also love saying "enableable") pic.twitter.com/vjXmquqrGW
While saying Enableable is cool and all; how in the world is it possible that the TimeCircuits::$enabled variable is
still false, when it was clearly updated on the Enableable trait? The answer is: a trait is not inherited.
Single Inheritance vs. composition
PHP is a so
called Single Inheritance language
meaning any class can inherit context from only one parent. In most cases this works out fine, but sometimes you
have code in a class you want to re-use. This is impossible if the class you want to use this on already extends
another class. This is why PHP introduced traits.
A trait is a kind of mini-class that can be used inside multiple classes. It can have methods, parameters, static
methods and static variables, just like a class. Even the visibility like private, protected and public works the
same. But instead inheriting from these traits by using extends, you have to use the trait inside a class. You
actually have to use a trait inside a class, because you cannot instantiate it on its own. A class can also use
multiple traits. This way you compose a new class made up of small re-usable pieces of code.
Traits are copy-pasted
While a trait looks like any other class, this use-ing instead of extending makes a big difference. When you use a
trait, the current state of the trait is copied to the class. And this copying is the reason why TimeCircuits is
unaffected by the change on the Enableable trait. It is not inheriting this variable; it has it's own copy of it.
And because the parameter was set to true before the FluxCapacitor class was created, this class has a copy of
that state. Resetting Enableable::$enabled to false at the end will therefore still have no impact on either
TimeCircuits or FluxCapacitor. They are completely separate parameters.
Classes inherit
So let's see how a class would react to a similar situation. We'll create a Base class that has a static $enabled
parameter, and an Extended class that extends (and therefore inherits from) Base. Then check out what happens when
we change the value of this variable.
class Base { public static $enabled = false;} class Extended extends Base {} Base::$enabled = true; var_dump(Base::$enabled, Extended::$enabled); // (bool) true, (bool) true
So here you see the inheritance in action. When we update the parameter on the Base class, it's extending classes are
affected by this update, because they are actually referencing the same parameter, and not a copy. And to prove that
they are referencing the same parameter you can change Extended::$enabled to true instead of Base::$enabled
and the result will still be the same.
Final notes
I'm not sure if Jess wanted a way to enable all classes that used this trait. If that is the case I see 2 alternatives:
- Let those classes extend from an intermediate class that has this parameter: for example a
Modelcould extend aEnableableModelthat has this parameter. In that case you could updateEnableableModel::$enabled. - Register an interface on all the classes that use this trait, and put them in a container. Then retrieve all classes from the container that have this interface, and update every one separately.
You might also be interested in my blog post on Testing Traits in PHPUnit. In this post I'll show you some handy tips and tricks for testing traits.