PHPnews.io

When to use a trait?

Written by Matthias Noback / Original link on Jul. 27, 2022

When to use a trait? Never.

Well, a trait could be considered to have a few benefits:

Benefits

  1. If you want to reuse some code between multiple classes, using a trait is an alternative for extending the class. In that case the trait may be the better option because it doesn't become part of the type hierarchy, i.e. a class that uses a trait isn't "an instance of that trait".
  2. A trait can save you some manual copy/paste-ing by offering compile-time copy/paste-ing instead.

Downsides

On the other hand, there are several problems with traits. For instance:

Better alternatives

Anyway, in practice, I always find better alternatives for using a trait. As an example, here are some alternatives:

A counter-example

Even though most situation don't really need a trait, and there are better alternatives, there is one situation that I keep using a trait for. This is the code for that trait, and if you ever attended one of my workshops, you'll know it already:

trait EventRecording
{
    /**
     * @var list<object>
     */
    private array $events = [];

    private function recordThat(object $event): void
    {
        $this->events[] = $event;
    }

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        $events = $this->events;

        $this->events = [];

        return $events;
    }
}

I use this trait in entities, because there I always want to do the same thing: record any number of events, then after saving the modified state of the entity, release those events in order to dispatch them.

One concern might be that releaseEvents() is a public method. But it doesn't have to be on any interface. We don't need an Entity interface, or a RecordsEvents interface, unless we want to create some reusable code that can save an entity and dispatch its recorded events.

Another concern is that this trait suffers from the lack of "trait-private". As an example, instead of using $this->recordThat(...), an entity that uses this trait could also just do $this->events[] = ....

We could fix this issue by extracting the code into an object, (adding even more evidence to the statement that there's always an alternative for using a trait):

final class EventRecording
{
    /**
     * @var list<object>
     */
    private array $events = [];

    public function recordThat(object $event): void
    {
        // ...
    }

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        // ...
    }
}

We then need to assign an instance of this new class to a private property of the entity:

final class SomeEntity
{
    // Can we do this already? I forgot
    private EventRecording $eventRecording = new EventRecording();

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        return $this->eventRecording->releaseEvents();
    } 

    // ...
}

Every entity would still need that public method releaseEvents(), and that additional private property, which we copy/paste into each new entity class. We could again introduce a trait for that:

trait ReleaseEvents
{
    private EventRecording $eventRecording = new EventRecording();

    /**
     * @return list<object>
     */
    public function releaseEvents(): array
    {
        return $this->eventRecording->releaseEvents();
    } 
}

At this point I feel like the extracted EventRecording class doesn't do too much anyway, and may be a considered a Lazy class (code smell). We might just as well use the original trait, accept the design concerns, and be done. Fine.

Request for Traits

Based on my experience with traits (having seen examples of them in projects, frameworks, and libraries), I don't know of any good case for using traits, so my rule is to never use them. But, you probably have some good examples of traits that make sense, and only make sense as a trait. So here is my Request for Traits. Please share your examples by posting a comment!

matthiasnoback

« PHP Blueprint - Introduction »