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
Model
could extend aEnableableModel
that 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.