The pitfalls of using traits

dakujem

Andrej Rypo

Posted on October 10, 2022

The pitfalls of using traits

When traits came to PHP, I'm sure many (myself included) thought they were a viable solution for code reuse. They offered ways to reduce code verbosity. They were easy to grasp. Suddenly we needed not to duplicate code across unrelated classes. 🤩 However, many (myself included) sobered after a hard lesson...

What are traits in PHP

In simple terms, a trait is a piece of code that gets copy-pasted into a class using it.
You can read what the PHP Manual says about traits if you are not familiar with them.

Traits came to my rescue

I had a bunch of classes that all needed the same: a Logger, a Connection and a Serializer instances (there were some other, it was about 6-8 such services IIRC). The constructors were bloated, there were ~50 lines of code just to assign these services to private props through a constructor, same comments, same everything, in every one of these classes...

With every query I would have to type $this->connection->table('foo')->select(...) and then $this->serializer->serialize($result) and whenever something needed logging $this->logger->log(stuff).

Thinking of introducing base classes, are we? But my target classes were not all related, it would not have solved the issue, and, worse, base classes stink.

So I experimented with "bundles"... I would inject a single class to the constructors, but then needed to type this kind of code $this->bundle->logger->log(stuff) 🤦‍♂️.

Instead, I discovered with traits I could type just $this->log(stuff), $this->select(...) or $this->serialize($result).

It was easy to write a trait with those methods, like this

protected function select(...$args) {
    return $this->connection
        ->use($this->database)
        ->table($this->table)
        ->select(...$args);
}

public function injectConnection(Connection $c){
    $this->connection = $c;
}
Enter fullscreen mode Exit fullscreen mode

There was some magic calling the setter automagically on startup.

I could even combine multiple such traits to create shortcuts to several services.

A perfect solution.
Except...

Except...

So... How do you test a trait? Well, you don't! You can't instantiate it, you test a class that uses the trait.
How do you know a class is using a trait, so that you can test it? Well, you don't1!
See the problem?

Except...

What about dependencies? You create a fancy class with half a dozen traits and as many services that you need to inject, somehow, and you end up with boilerplate code nevertheless.
Worse, you create hidden dependences. And wait for the moment you start combining traits into supertraits.

Takeaway

Think twice before creating and/or using a trait. I dare say, in most cases, if you need a trait, something in the class design has to smell.

Remember that using a trait is just copy-pasting code that could otherwise sit in a proper class.

Use class composition instead of traits. Use DI containers with autowiring capabilities to reduce boilerplate. Understand that typing a couple of characters extra is cheaper than dealing with tangled dependencies and trait conflicts.
In fact, those "bundles" I mentioned earlier, if executed correctly2, are the way to go in many cases.

Traits are a valid language construct, though. Do use them where applicable. Make a conscious choice and be content with it.


  1. Unless you inspect the source or use reflection. 

  2. Yes, I mean interfaces. 

💖 💪 🙅 🚩
dakujem
Andrej Rypo

Posted on October 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related