Back in those days when I worked intensively with Laravel, I wrote an article about PHP traits and when to use them. I outlined why traits in PHP are a bad compromise to bypass the “limitation” of single inheritance and which problems may occur when using traits intensively. Lastly, I gave my personal opinion about traits and showed some alternatives.
In this post, I want to make a recap, explore what have been changed since the last article and evaluate the status quo of the trait implementation in PHP.
Recap: What are PHP Traits?
Traits introduced to PHP in version 5.4 are a bad compromise designed to bypass the “limitation” of single inheritance. A class, or even a trait itself, can use multiple other traits, leading to the possibility of encountering the diamond problem, as well as the potential to create a series of traits, possibly even in a loop, resulting in technically valid yet complex code.
Additionally, traits do not support private or protected methods/members. They often operate under the assumption that certain methods/members exist and use them in a way similar to private methods in a usual class. From an object-oriented programming perspective, traits do not have an “identity,” making it impossible to use the
instanceof operator for checks.
What has changed in Traits?
PHP 8.0 added the ability to define abstract methods including signature check, PHP 8.2 introduced constants and the current version of PHP as of this posts publish day, 8.3, makes a change in how traits and its parent classes handles static properties1.
Interpreting the Impact
First things first: the changes made the situation worse.
In the context of SOLID principles and clean code architecture, two out of the three additions do not align well. Abstractions in methods and classes suggest a dependence on inheritance, and the use of static properties or methods are in almost every scenario the death sentence of robustness, predictability, and cleanliness of any source code or architecture.
But why? Well, let’s talk about the most important arguments not using traits.
- Single Responsibility Principle (SRP) Violation: Misusing traits can result in classes with multiple responsibilities due to numerous traits, breaching SRP and decreasing code readability and maintainability.
- Breaking Architecture Design: Misusing traits to quickly introduce features can result in a “patchwork” codebase, lacking clear structure and making it hard to grasp the relationships and duties of various classes.
- Hidden Class Dependencies: Since chaining is allowed with traits, class/trait relations can get unclear very quickly.
- Hard to Test: Traits can not get instantiated, and thus they can not get tested isolated (a.k.a. unit tested). Combining with the fact that multiple traits can play a role in a test case, testing will get a challenge.
If you are a returning reader on my blog, you will most likely have noticed I am a strong advocate of Dependency Injection (short: DI). In fact, I believe there is no realistic alternative. Every time I have implemented traits (the good old Laravel days), I ended up in refactoring to Dependency Injection instead. The key advantage is the flexibility to instantiate code separately. Rounding up with implementing against interfaces instead of real classes, the code base gets loosely coupled and concrete implementation is just a detail.
Moreover, managing numerous
use statements to import traits can lead to refactoring challenges. If you relocate a trait, it ends up in updating every class that uses it. On the other hand, with DI, you simply supply the object from a central point, like the composition root. This means moving the original class file will not disrupt your application in other areas.
Back in my first blog, I finished the post outlining Dependency Injection as an alternative. This still has not changed and I think avoiding traits and having an architecture based on DI is the key for scalable, robust and predictable systems.
I am not just saying this on a theoretical basis: with Keestash, we proved this multiple times with multiple refactorings and code adjustments. Imagine Keestash in layers: the top most layer is the request handler that handles HTTP requests. This layer gets a lot of dependencies as interfaces injected and serves as a loose composition of different services. The concrete implementation of those services is part of the layers below: object generation, database handling, email dispatching are just a few of them.
Another benefit of DI/interface based architecture is with unit and integration tests: the Keestash Test Suite overwrites just a couple of classes and binds them against the interfaces used in the request handler. This way, avoiding email dispatching during integration tests is just a question of proper configuration.
In my career as a freelancer, I had a couple of projects and customers so far. I experienced legacy code and state of the art projects. If you are interested in my freelance services, please use the contact formula below. I will get back to you as soon as possible.