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.
// 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
usemultiple traits — comma-separated:use TraitA, TraitB; - A trait can itself
useother traits
Using multiple traits at once
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"
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:
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.
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:
┌──────────────────────────────────┬───────────┬───────────┬────────────┐
│ 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.
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. }
// 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();
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:
// 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?