DEV Community

Cover image for Why I Avoid PHP Traits (And What I Use Instead)
Ivan Mykhavko
Ivan Mykhavko

Posted on

Why I Avoid PHP Traits (And What I Use Instead)

PHP traits are usually presented as a handy way to reuse code. In practice, they are one of the most tricky tools in PHP and they can easily break your architecture, your tests, and your code readability.

I'm not saying traits are absolute evil. But after years of working with PHP, I kept coming to the same conclusion: traits are a design smell.

What Traits Actually Are

Traits appeared as a compromise to work around single inheritance. They are not inheritance, not composition, and not an interface. In short, it's a way to "glue" code into a class without explicit dependencies. And that's exactly where the problems start. On the low level, it works like simple copy-paste.

Why They Cause Problems

Poor readability. When I see a class with use SomeTrait, I don't know what that class actually does. To understand it, I need to open the trait, check what protected methods and properties it expects, and figure out if it overrides something. The behavior of the class becomes non-obvious.

Hidden coupling. In real code, traits almost always use protected properties of the class or call protected methods that the class doesn't implement itself. The result is two-way, implicit coupling - the trait knows about the class internals, and the class depends on the trait internals. You can't see this from the constructor or method signatures.

Broken encapsulation. Traits encourage access to the internal state of a class. Instead of clear contracts and explicit dependencies, you get: "the trait expects that somewhere there is a $service". Change the internal structure and the trait breaks.

Hard to test. You can't instantiate a trait. To test it, you need to create a fake class, set up all protected dependencies, and hope you didn't miss anything. That's not a unit test, it's a workaround.

Architectural chaos. PHP allows traits inside traits, multiple traits in one class. This makes it very easy to end up with diamond problems, dependency chains, and code that "works" but nobody understands how.

What About PHP 8.x Improvements?

PHP 8 added abstract methods, constants, and changes to static properties in traits. Unfortunately, from a SOLID and clean architecture point of view, this made things worse - traits now pull even deeper into inheritance thinking and static state.

The Typical Smell

If a trait has protected methods and uses protected services from the class, it almost always means there is a missing separate object that should be extracted into its own class.

What I Use Instead

In 90% of cases, the answer is simple:

  • Dependency Injection - dependencies are explicit, code is readable from the constructor
  • Composition - "has-a" instead of "uses"
  • Strategy / Factory - especially instead of protected helper methods
  • A simple class or function - if the trait has no state, it's probably not needed

All of these make dependencies visible, improve testability, and keep the architecture clean.

Quick Refactor Example

Before (with trait):

trait NotifiableTrait
{
    protected function notifyUser(string $message)
    {
        $this->notificationService->send($this->user, $message);
    }
}

final class OrderCreateAction
{
    use NotifiableTrait;

    public function handle(OrderCreateDTO $dto)
    {
        ...order logic...

        $this->notifyUser('Order created!'); // where is this from?
    }
}
Enter fullscreen mode Exit fullscreen mode

After (with DI):

final class OrderCreateAction
{    
    public function __construct(private readonly UserNotifier $notifier) {}

    public function handle(OrderCreateDTO $dto)
    {
        ...order logic...

        $this->notifier->send($user, 'Order created!'); // explicit and clear
    }
}
Enter fullscreen mode Exit fullscreen mode

Now dependencies are visible, testable, and explicit.

Conclusion

Traits are a shortcut that often leads to a long refactor later. If behavior can be extracted into a separate object, it should be extracted. If a trait has no state, it probably doesn't need to exist. I don't forbid traits. I just try not to pay for them later.

References

Author's Note

Thanks for sticking around!
Find me on dev.to, linkedin, or you can check out my work on github.

Notes from real-world Laravel.

Top comments (0)