PHP::OOP_Mastery
Overview / Session 06 / Session 07
Session 07 of 10  ·  2 hrs + 15 min review

PHP Traits

⏱ 2-hour session 📋 15-min review ⚠ Requires Sessions 01–06 🗂 4 topics

1 — What Traits Are and Why PHP Needs Them

PHP has single inheritance — a class can only extend one parent. This becomes a real problem when two unrelated class hierarchies need the same piece of behaviour. Consider User and Post: they extend nothing in common, yet both need timestamp tracking (created_at, updated_at) and soft-delete logic. Where does that shared code live?

The naive answer — a BaseModel parent class — forces an artificial inheritance relationship and piles unrelated concerns into one class. Traits are PHP's purpose-built answer: a horizontal code-reuse mechanism that lets you compose behaviour into any class, regardless of its inheritance chain.

Think of a trait as a named block of copy-paste that the compiler handles for you. When you use a trait inside a class, PHP literally inlines the trait's methods and properties into that class at compile time — as if you had written them there yourself.

PHP — Your first trait
// Declare a trait — same syntax as a class, but with `trait`
trait HasTimestamps
{
    private ?DateTimeImmutable $createdAt = null;
    private ?DateTimeImmutable $updatedAt = null;

    public function initTimestamps(): void
    {
        $this->createdAt = new DateTimeImmutable();
        $this->updatedAt = new DateTimeImmutable();
    }

    public function touch(): void
    {
        $this->updatedAt = new DateTimeImmutable();
    }

    public function getCreatedAt(): ?DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): ?DateTimeImmutable
    {
        return $this->updatedAt;
    }
}

// Use it in any class — regardless of what it extends
class User
{
    use HasTimestamps;   // ← one line pulls in 4 methods + 2 properties

    public function __construct(private string $email)
    {
        $this->initTimestamps();   // method came from the trait
    }
}

class Post
{
    use HasTimestamps;   // same trait, completely unrelated class

    public function __construct(private string $title)
    {
        $this->initTimestamps();
    }
}

$user = new User('alice@example.com');
echo $user->getCreatedAt()->format('Y-m-d');   // today's date

$post = new Post('Hello World');
echo $post->getCreatedAt()->format('Y-m-d');   // same trait, different class

Trait rules at a glance

  • Declared with trait NameOfTrait — PascalCase, like a class
  • Cannot be instantiated directly — new HasTimestamps() is a fatal error
  • Can contain properties, methods (any visibility), abstract methods, and constants (PHP 8.2+)
  • Cannot implement interfaces or extend classes on their own
  • A class can use multiple traits — comma-separated: use TraitA, TraitB;
  • A trait can itself use other traits

Using multiple traits at once

PHP — Multiple traits in one class
trait SoftDeletes
{
    private ?DateTimeImmutable $deletedAt = null;

    public function softDelete(): void
    {
        $this->deletedAt = new DateTimeImmutable();
    }

    public function restore(): void
    {
        $this->deletedAt = null;
    }

    public function isDeleted(): bool
    {
        return $this->deletedAt !== null;
    }

    public function getDeletedAt(): ?DateTimeImmutable
    {
        return $this->deletedAt;
    }
}

trait HasUuid
{
    private string $uuid;

    public function initUuid(): void
    {
        // PHP 8.3 has Uuid support; pre-8.3 use ramsey/uuid
        $this->uuid = sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public function getUuid(): string { return $this->uuid; }
}

// Article uses THREE traits — inherits all their methods and properties
class Article
{
    use HasTimestamps, SoftDeletes, HasUuid;

    public function __construct(private string $title)
    {
        $this->initUuid();
        $this->initTimestamps();
    }
}

$article = new Article('PHP Traits Deep Dive');
echo     $article->getUuid();       // from HasUuid
echo     $article->getCreatedAt(); // from HasTimestamps
var_dump($article->isDeleted());  // bool(false) — from SoftDeletes

$article->softDelete();
var_dump($article->isDeleted());  // bool(true)

2 — Conflict Resolution: insteadof & as

When you use two traits that both define a method with the same name, PHP cannot decide which one to use — it throws a fatal error unless you resolve the conflict explicitly. Two keywords handle this:

  • insteadof — "use this trait's version, not that one's"
  • as — "keep both, but give one an alias"
PHP — Conflict resolution with insteadof and as
trait JsonSerializer
{
    public function serialize(): string
    {
        return json_encode(get_object_vars($this));
    }
}

trait XmlSerializer
{
    public function serialize(): string
    {
        // simplified XML output for example
        $tag = strtolower(basename(str_replace('\\', '/', static::class)));
        return "<{$tag}>" . implode('', array_map(
            fn($k, $v) => "<{$k}>{$v}</{$k}>",
            array_keys(get_object_vars($this)),
            get_object_vars($this)
        )) . "</{$tag}>";
    }
}

class Product
{
    use JsonSerializer, XmlSerializer {
        // "When serialize() conflicts, prefer JsonSerializer's version"
        JsonSerializer::serialize insteadof XmlSerializer;

        // "Still keep XmlSerializer's version, but call it serializeXml()"
        XmlSerializer::serialize as serializeXml;
    }

    public function __construct(
        public string $name,
        public float  $price
    ) {}
}

$p = new Product('Keyboard', 79.99);

echo $p->serialize();
// {"name":"Keyboard","price":79.99}  ← JsonSerializer won the conflict

echo $p->serializeXml();
// <product><name>Keyboard</name><price>79.99</price></product>

Changing visibility with as

The as keyword does double duty — it can also change the visibility of a trait method without renaming it. This is useful when a trait exposes a method as public but you want to hide it in a specific class:

PHP — Changing visibility with as
trait Greetable
{
    public function greet(): string
    {
        return "Hello, I am {$this->name}";
    }
}

class InternalService
{
    use Greetable {
        greet as protected;   // demote from public to protected
    }

    public string $name = 'InternalService';
}

// $service->greet() is now protected — external code can't call it

Conflict resolution in multi-level trait composition

Conflicts only arise when two traits used in the same class define the same method name. If a trait uses another trait internally, and both define the same method, the outer trait's resolution rules apply first. The class-level insteadof block is always the final word.

3 — Abstract Methods in Traits

A trait can declare abstract methods — methods with no body that the using class must implement. This is a powerful pattern: the trait provides behaviour that depends on something the class knows and the trait doesn't. It's a contract between the trait and its host.

PHP — Trait with abstract method requirement
trait Validatable
{
    // The trait calls this method internally — the class must provide it
    abstract protected function rules(): array;

    public function validate(array $data): array
    {
        $errors = [];
        foreach ($this->rules() as $field => $rule) {
            if ($rule === 'required' && empty($data[$field] ?? null)) {
                $errors[] = "{$field} is required";
            }
            if (str_starts_with($rule, 'min:')) {
                $min = (int) substr($rule, 4);
                if (strlen($data[$field] ?? '') < $min) {
                    $errors[] = "{$field} must be at least {$min} chars";
                }
            }
        }
        return $errors;
    }

    public function isValid(array $data): bool
    {
        return empty($this->validate($data));
    }
}

class RegistrationForm
{
    use Validatable;

    // Must implement rules() — the trait demands it
    protected function rules(): array
    {
        return [
            'username' => 'required',
            'email'    => 'required',
            'password' => 'min:8',
        ];
    }
}

class LoginForm
{
    use Validatable;

    protected function rules(): array
    {
        return [
            'email'    => 'required',
            'password' => 'required',
        ];
    }
}

$form   = new RegistrationForm();
$errors = $form->validate(['username' => 'alice', 'email' => '', 'password' => 'abc']);
// ["email is required", "password must be at least 8 chars"]

This pattern mirrors the Template Method from Session 05 — the trait defines the algorithm skeleton; the class fills in the specifics. The difference from an abstract class: the class is free to extend anything else, and can use multiple traits that each demand their own abstract methods.

4 — Real Use-Cases: Traits vs Abstract Classes vs Interfaces

You now know all three horizontal/vertical composition tools. Here's when each one wins:

Decision guide
┌──────────────────────────────────┬───────────┬───────────┬────────────┐
│ Need                             │ Interface │ Abstract  │   Trait    │
├──────────────────────────────────┼───────────┼───────────┼────────────┤
│ Type for dependency injection    │    ★★★    │    ★★     │     ✗      │
│ Force method signature contract  │    ★★★    │    ★★★    │    ★★      │
│ Share concrete implementation    │     ✗     │    ★★     │    ★★★     │
│ Multiple "inheritance"           │    ★★★    │     ✗     │    ★★★     │
│ Works across unrelated classes   │    ★★★    │     ✗     │    ★★★     │
│ Contains properties              │     ✗     │    ★★★    │    ★★★     │
│ Clearly communicates "is-a"      │     ✗     │    ★★★    │     ✗      │
└──────────────────────────────────┴───────────┴───────────┴────────────┘

Laravel-style production patterns

Here are four traits you'll see in virtually every real PHP project — study these and you'll recognise them immediately in Laravel, Symfony, and beyond.

PHP — HasEvents trait (Observer pattern helper)
trait HasEvents
{
    private array $pendingEvents = [];

    protected function raise(object $event): void
    {
        $this->pendingEvents[] = $event;
    }

    public function releaseEvents(): array
    {
        $events = $this->pendingEvents;
        $this->pendingEvents = [];
        return $events;
    }
}

// A domain event — just a plain class
class UserRegistered
{
    public function __construct(public readonly string $email) {}
}

class User
{
    use HasTimestamps, HasEvents;

    public function __construct(private string $email)
    {
        $this->initTimestamps();
        $this->raise(new UserRegistered($email));  // record the event
    }
}

// After saving to the DB, dispatch all raised events:
$user   = new User('alice@example.com');
$events = $user->releaseEvents();
foreach ($events as $event) {
    $eventBus->dispatch($event);   // send welcome email, trigger webhooks, etc.
}
PHP — Singleton trait (reusable pattern)
// Instead of re-writing Singleton logic in every class, put it in a trait
trait SingletonTrait
{
    private static ?static $instance = null;

    private function __construct() {}
    private function __clone() {}

    public static function getInstance(): static
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }
        return static::$instance;
    }
}

class AppConfig   { use SingletonTrait; /* ... */ }
class EventBus    { use SingletonTrait; /* ... */ }
class PluginRegistry { use SingletonTrait; /* ... */ }

// Each class gets Singleton behaviour with a single line:
$config = AppConfig::getInstance();
$bus    = EventBus::getInstance();
PHP — Comparable trait (sorting objects)
trait Comparable
{
    // Class must provide this — returns the value used for comparison
    abstract protected function comparableValue(): mixed;

    public function lessThan(static $other): bool
    {
        return $this->comparableValue() < $other->comparableValue();
    }

    public function greaterThan(static $other): bool
    {
        return $this->comparableValue() > $other->comparableValue();
    }

    public function equalTo(static $other): bool
    {
        return $this->comparableValue() === $other->comparableValue();
    }
}

class Temperature
{
    use Comparable;

    public function __construct(private float $celsius) {}

    protected function comparableValue(): mixed
    {
        return $this->celsius;
    }
}

$boiling  = new Temperature(100);
$freezing = new Temperature(0);

var_dump($boiling->greaterThan($freezing)); // bool(true)
var_dump($freezing->lessThan($boiling));   // bool(true)

// Any class can get comparison operators with one line + one abstract method

Traits don't establish type relationships

This is the most important limitation to understand: using a trait does not make a class satisfy an interface or be a subtype of anything. You cannot type-hint a trait name in a parameter — only class and interface names work there. If you need both shared code and a type contract, combine a trait with an interface:

PHP — Trait + Interface: the complete pattern
// Interface: the type contract used in type hints
interface Auditable
{
    public function getCreatedAt(): ?DateTimeImmutable;
    public function getUpdatedAt(): ?DateTimeImmutable;
    public function touch(): void;
}

// Trait: the shared implementation
trait AuditableTrait
{
    use HasTimestamps;  // traits can use other traits
}

// Class: satisfies the type contract AND gets the implementation for free
class User implements Auditable
{
    use AuditableTrait;

    public function __construct(private string $email)
    {
        $this->initTimestamps();
    }
}

// Now User is type-compatible with Auditable AND has the implementation:
function logActivity(Auditable $entity): void
{
    echo "Last updated: " . $entity->getUpdatedAt()->format('Y-m-d H:i:s');
}

logActivity(new User('alice@example.com'));  // works perfectly

This Interface + Trait combination is the most powerful and flexible pattern in modern PHP. The interface gives you the type, the trait gives you the implementation. Laravel's Eloquent model uses exactly this approach for dozens of features: SoftDeletes, HasFactory, HasTimestamps, HasEvents — all traits.

15-Minute Review — Session 07

Switch the sidebar timer to review mode. Answer from memory — no scrolling.

Q1 — Why does PHP have traits at all?

Q2 — Two traits both define a format() method. You use both in one class. What happens without a conflict resolution block?

Q3 — What does TraitA::method as aliasMethod; do?

Q4 — Can you type-hint a trait name in a function parameter?

Q5 — A trait declares an abstract protected function rules(): array;. What does this mean for any class that uses this trait?

✓ Key concepts checklist

Next: Magic Methods →