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

Published: (February 2, 2026 at 08:21 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

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, they are a way to “glue” code into a class without explicit dependencies. 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. I have to open the trait, check what protected methods and properties it expects, and see if it overrides something. The behavior becomes non‑obvious.
  • Hidden coupling – Traits often use protected properties of the class or call protected methods that the class doesn’t implement itself. This creates 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”. Changing the internal structure breaks the trait.
  • 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 and multiple traits in one class. This makes it easy to end up with diamond problems, tangled 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 perspective, 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 most cases, the solution 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?
    }
}

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
    }
}

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

Back to Blog

Related posts

Read more »