0

Mixins via Traits — composition over inheritance

Intermediate5 min read·php-08-012
comparesolid

Concept

Mixins via Traits are the PHP pattern for horizontal composition — sharing behavior across classes that have no inheritance relationship. The phrase "composition over inheritance" is directly embodied by traits: instead of extending a base class to gain behavior, you mix in a trait.

The inheritance problem traits solve: If User, Product, and Order all need audit logging, you can't make them all extend AuditableBase — they likely already extend different parents (Eloquent Model, etc.). A trait HasAuditLog can be used by all three without touching their inheritance chains.

Interface + Trait combination: The idiomatic Laravel/PHP pattern: define an interface for the contract, a trait for the default implementation. The class implements the interface (for type-checking) and uses the trait (for the implementation). The trait can reference abstract methods it expects the using class to provide.

Trait limitations vs full inheritance: Traits cannot define their own constants (PHP < 8.2). They don't participate in instanceof checks (a class using HasTimestamps is not "instanceof HasTimestamps"). They create implicit coupling — a trait that calls $this->id assumes the class has an id property, but the compiler doesn't verify this. This is why abstract requirements in traits are valuable.

Eloquent's use of traits: SoftDeletes, HasTimestamps, Notifiable, HasFactory, Searchable, HasApiTokens — all implemented as traits mixed into models. This lets models opt into features without inheritance coupling.

Code Example

php
<?php
declare(strict_types=1);

// Interface contract
interface Versionable
{
    public function getVersion(): int;
    public function incrementVersion(): void;
}

// Trait provides default implementation
trait HasVersioning
{
    private int $version = 1;

    public function getVersion(): int
    {
        return $this->version;
    }

    public function incrementVersion(): void
    {
        $this->version++;
    }

    public function resetVersion(): void
    {
        $this->version = 1;
    }
}

// Document and Product both "have" versioning — no inheritance needed
class Document implements Versionable
{
    use HasVersioning;
    public function __construct(public string $title) {}
}

class Product implements Versionable
{
    use HasVersioning;
    public function __construct(public string $name, public float $price) {}
}

$doc = new Document('PHP Guide');
$doc->incrementVersion();
$doc->incrementVersion();
echo $doc->getVersion(); // 3

// Trait with abstract method requirement
trait HasSlug
{
    abstract public function getSlugSource(): string; // implementing class must have this

    private ?string $slug = null;

    public function getSlug(): string
    {
        if ($this->slug === null) {
            $source = $this->getSlugSource(); // uses abstract from host class
            $this->slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $source));
        }
        return $this->slug;
    }
}

class BlogPost
{
    use HasSlug;

    public function __construct(private string $title) {}

    public function getSlugSource(): string { return $this->title; }
}

$post = new BlogPost('Hello World! PHP 8.4');
echo $post->getSlug(); // "hello-world-php-8-4"