Traits #
PHP only supports single inheritance — one class can only extend one parent class. This means that if you have two unrelated classes, like User and Product, but both need the same feature such as automatic timestamps, soft delete, or serialization capability, you can’t share that code through inheritance without forcing an unreasonable hierarchy. Traits are PHP’s solution to this problem: a horizontal code reuse mechanism that lets you insert methods and properties into any class, regardless of its hierarchy. This article covers traits in depth — how they work, real patterns commonly used in frameworks like Laravel, how to handle conflicts when two traits have the same method, and — just as importantly — when traits become a problem and should be avoided.
Why Traits Exist — The Problem They Solve #
Imagine three unrelated classes that all need the same features:
flowchart TD
A[Article] -- "needs" --> T1[Timestamp\ncreated_at, updated_at]
B[Product] -- "needs" --> T1
C[Comment] -- "needs" --> T1
A -- "needs" --> T2[SoftDelete\ndeleted_at]
B -- "needs" --> T2
D[User] -- "needs" --> T3[HasUuid\nid as UUID]
B -- "needs" --> T3
style T1 fill:#dcfce7
style T2 fill:#fef9c3
style T3 fill:#dbeafeWithout traits, there are two equally bad options: duplicating code in every class, or forcing an unnatural inheritance hierarchy (making a BaseModel that contains every feature, even though not every model needs every feature). Traits offer a third option: insert only the features you need into the classes that need them.
Defining and Using Traits #
A trait is defined with the trait keyword. A class uses it with the use keyword inside the class body — not at the top of the file like use for namespaces.
<?php
trait Timestampable
{
private ?\DateTime $createdAt = null;
private ?\DateTime $updatedAt = null;
public function setCreatedAt(\DateTime $dt): void
{
$this->createdAt = $dt;
}
public function setUpdatedAt(\DateTime $dt): void
{
$this->updatedAt = $dt;
}
public function getCreatedAt(): ?\DateTime
{
return $this->createdAt;
}
public function getUpdatedAt(): ?\DateTime
{
return $this->updatedAt;
}
public function touch(): void
{
$now = new \DateTime();
if ($this->createdAt === null) {
$this->createdAt = $now;
}
$this->updatedAt = $now;
}
}
// Two unrelated classes, both using the same trait
class Article
{
use Timestampable;
public function __construct(
private string $title,
private string $body,
) {
$this->touch(); // from the trait
}
}
class Product
{
use Timestampable;
public function __construct(
private string $name,
private float $price,
) {
$this->touch(); // from the trait — same method, different class
}
}
$article = new Article("Learning PHP", "Article content...");
$product = new Product("Laptop", 15_000_000);
echo $article->getCreatedAt()->format('Y-m-d H:i:s'); // current time
echo $product->getUpdatedAt()->format('Y-m-d H:i:s'); // current time
Once a trait is used, all its methods and properties become as if they were defined directly in the class. There’s no difference from the outside — callers can’t tell whether touch() came from a trait or from the class itself.
Traits with Properties #
Traits can define properties, but there’s one important rule: if the class using the trait also defines a property with the same name, the type and visibility must be identical, otherwise PHP throws an error.
<?php
trait HasUuid
{
private string $uuid;
public function initUuid(): void
{
// Create a simple UUID v4
$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;
}
}
trait SoftDeletable
{
private ?\DateTime $deletedAt = null;
public function delete(): void
{
$this->deletedAt = new \DateTime();
}
public function restore(): void
{
$this->deletedAt = null;
}
public function isDeleted(): bool
{
return $this->deletedAt !== null;
}
public function getDeletedAt(): ?\DateTime
{
return $this->deletedAt;
}
}
// Use several traits at once
class User
{
use HasUuid, Timestampable, SoftDeletable;
public function __construct(
private string $name,
private string $email,
) {
$this->initUuid();
$this->touch();
}
}
$user = new User("Budi Santoso", "[email protected]");
echo $user->getUuid(); // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
echo $user->getCreatedAt()->format('Y-m-d'); // today
echo $user->isDeleted() ? "deleted" : "active"; // active
$user->delete();
echo $user->isDeleted() ? "deleted" : "active"; // deleted
$user->restore();
echo $user->isDeleted() ? "deleted" : "active"; // active
Traits with Abstract Methods #
A trait can define abstract methods that force the classes using it to provide an implementation. This is useful when the trait needs data from the class but doesn’t know how to get it:
<?php
trait Serializable
{
// The trait forces the class to provide this
abstract protected function getSerializableAttributes(): array;
public function toArray(): array
{
$result = [];
foreach ($this->getSerializableAttributes() as $attribute) {
$result[$attribute] = $this->$attribute ?? null;
}
return $result;
}
public function toJson(int $flags = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $flags);
}
public static function fromArray(array $data): static
{
$obj = new static();
foreach ($data as $key => $value) {
if (property_exists($obj, $key)) {
$obj->$key = $value;
}
}
return $obj;
}
}
class Product
{
use Serializable;
public function __construct(
private int $id = 0,
private string $name = '',
private float $price = 0,
private int $stock = 0,
) {}
// Implement the trait's abstract method
protected function getSerializableAttributes(): array
{
return ['id', 'name', 'price', 'stock'];
}
}
$product = new Product(1, 'Laptop', 15_000_000, 5);
echo $product->toJson();
// {"id":1,"name":"Laptop","price":15000000,"stock":5}
$fromArray = Product::fromArray(['id' => 2, 'name' => 'Mouse', 'price' => 250000]);
echo $fromArray->toJson();
// {"id":2,"name":"Mouse","price":250000,"stock":0}
Resolving Method Conflicts #
When two traits used by the same class have methods with identical names, PHP doesn’t know which one to use and throws a fatal error. There are two ways to resolve it: insteadof to choose one, and as to create an alias:
<?php
trait LoggerA
{
public function log(string $message): void
{
echo "[A] $message\n";
}
public function debug(string $message): void
{
echo "[A-DEBUG] $message\n";
}
}
trait LoggerB
{
public function log(string $message): void
{
echo "[B] $message\n";
}
public function debug(string $message): void
{
echo "[B-DEBUG] $message\n";
}
}
class Application
{
use LoggerA, LoggerB {
// insteadof — choose LoggerA::log, ignore LoggerB::log
LoggerA::log insteadof LoggerB;
// insteadof — choose LoggerB::debug, ignore LoggerA::debug
LoggerB::debug insteadof LoggerA;
// as — create an alias for the "defeated" method
// so it can still be accessed under a different name
LoggerB::log as logB;
LoggerA::debug as debugA;
}
}
$app = new Application();
$app->log("main message"); // [A] main message — LoggerA wins
$app->logB("via alias"); // [B] via alias — LoggerB via alias
$app->debug("debug info"); // [B-DEBUG] debug info — LoggerB wins
$app->debugA("detail"); // [A-DEBUG] detail — LoggerA via alias
Changing Visibility with as
#
as can also be used to change the visibility of trait methods:
<?php
trait FormatterTrait
{
public function formatRupiah(float $value): string
{
return 'Rp ' . number_format($value, 0, ',', '.');
}
public function formatPercent(float $value): string
{
return number_format($value * 100, 1) . '%';
}
}
class FinancialReport
{
use FormatterTrait {
// Change visibility — the method becomes protected, for subclasses only
formatRupiah as protected;
// Create an alias with a different visibility
formatPercent as public calculatePercent;
}
public function print(float $price, float $discount): void
{
// formatRupiah can be called from inside the class (protected OK)
echo $this->formatRupiah($price) . "\n";
echo $this->calculatePercent($discount) . "\n";
}
}
$report = new FinancialReport();
$report->print(15_000_000, 0.1);
// Rp 15.000.000
// 10.0%
// $report->formatRupiah(1000); // Fatal Error — now protected
$report->calculatePercent(0.2); // "20.0%" — the alias stays public
Traits Using Other Traits #
A trait can use other traits — this enables more modular trait composition:
<?php
trait HasTimestamp
{
private ?\DateTime $createdAt = null;
private ?\DateTime $updatedAt = null;
public function touchTimestamp(): void
{
$now = new \DateTime();
$this->createdAt ??= $now;
$this->updatedAt = $now;
}
public function getCreatedAt(): ?\DateTime { return $this->createdAt; }
public function getUpdatedAt(): ?\DateTime { return $this->updatedAt; }
}
trait HasSoftDelete
{
private ?\DateTime $deletedAt = null;
public function softDelete(): void { $this->deletedAt = new \DateTime(); }
public function restore(): void { $this->deletedAt = null; }
public function isTrashed(): bool { return $this->deletedAt !== null; }
}
// A combined trait — using two traits at once
trait ModelTrait
{
use HasTimestamp, HasSoftDelete;
// Add a method that combines both
public function toMeta(): array
{
return [
'created_at' => $this->createdAt?->format('Y-m-d H:i:s'),
'updated_at' => $this->updatedAt?->format('Y-m-d H:i:s'),
'deleted_at' => $this->deletedAt?->format('Y-m-d H:i:s'),
];
}
}
// The class only uses one trait, but gets every feature
class Article
{
use ModelTrait;
public function __construct(private string $title)
{
$this->touchTimestamp();
}
}
$article = new Article("Learning PHP Traits");
print_r($article->toMeta());
// Array ( [created_at] => 2024-01-15 10:30:00 [updated_at] => ... [deleted_at] => )
Common Trait Patterns in Frameworks #
PHP frameworks like Laravel use traits extensively. Here are some of the most frequently encountered patterns:
Pattern: Singleton via Trait #
<?php
trait Singleton
{
private static ?self $instance = null;
// Prevent direct instantiation
private function __construct() {}
private function __clone() {}
public static function getInstance(): static
{
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
}
class Config
{
use Singleton;
private array $data = [];
public function set(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
}
$config1 = Config::getInstance();
$config2 = Config::getInstance();
$config1->set('debug', true);
echo $config2->get('debug') ? 'true' : 'false'; // true — the same instance
var_dump($config1 === $config2); // bool(true)
Pattern: Observable via Trait #
<?php
trait Observable
{
private array $listeners = [];
public function on(string $event, callable $callback): void
{
$this->listeners[$event][] = $callback;
}
protected function emit(string $event, mixed ...$args): void
{
foreach ($this->listeners[$event] ?? [] as $callback) {
$callback(...$args);
}
}
}
class Order
{
use Observable;
private string $status = 'pending';
public function setStatus(string $status): void
{
$oldStatus = $this->status;
$this->status = $status;
// Emit the event — all listeners get called
$this->emit('statusChanged', $oldStatus, $status, $this);
}
public function getStatus(): string
{
return $this->status;
}
}
$order = new Order();
// Register listeners
$order->on('statusChanged', function(string $old, string $new, Order $order) {
echo "Order changed from '$old' to '$new'\n";
});
$order->on('statusChanged', function(string $old, string $new, Order $order) {
// Send email notification, logs, etc.
echo "Sending notification for status: $new\n";
});
$order->setStatus('processing');
// Order changed from 'pending' to 'processing'
// Sending notification for status: processing
$order->setStatus('shipped');
// Order changed from 'processing' to 'shipped'
// Sending notification for status: shipped
Pattern: Method Chaining via Trait #
<?php
trait Fluent
{
// Enables method chaining by returning $this
protected function set(string $property, mixed $value): static
{
$this->$property = $value;
return $this;
}
}
class QueryBuilder
{
use Fluent;
private string $table = '';
private array $wheres = [];
private ?int $limit = null;
private ?int $offset = null;
private string $orderBy = '';
public function from(string $table): static
{
return $this->set('table', $table);
}
public function where(string $condition): static
{
$this->wheres[] = $condition;
return $this;
}
public function limit(int $limit): static
{
return $this->set('limit', $limit);
}
public function offset(int $offset): static
{
return $this->set('offset', $offset);
}
public function orderBy(string $column, string $direction = 'ASC'): static
{
return $this->set('orderBy', "$column $direction");
}
public function build(): string
{
$sql = "SELECT * FROM {$this->table}";
if (!empty($this->wheres)) {
$sql .= " WHERE " . implode(' AND ', $this->wheres);
}
if ($this->orderBy) {
$sql .= " ORDER BY {$this->orderBy}";
}
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
return $sql;
}
}
$query = (new QueryBuilder())
->from('products')
->where('stock > 0')
->where('price < 5000000')
->orderBy('price', 'ASC')
->limit(10)
->offset(0)
->build();
echo $query;
// SELECT * FROM products WHERE stock > 0 AND price < 5000000 ORDER BY price ASC LIMIT 10 OFFSET 0
Trait vs Interface vs Abstract Class #
All three can look similar on the surface, but each has a different role:
| Aspect | Trait | Interface | Abstract Class |
|---|---|---|---|
| Can be instantiated | ✗ | ✗ | ✗ |
| Contains implementations | ✓ | ✗ | ✓ (partial) |
| Contains properties | ✓ | ✗ | ✓ |
| Contains constants | ✓ (PHP 8.2+) | ✓ | ✓ |
| Number that can be used | Many | Many | One |
Forms a type (instanceof) | ✗ | ✓ | ✓ |
| Represents | Copied capability | Contract / type | Base type + shared implementation |
The most critical difference: traits don’t form a type. A class that uses a Timestampable trait is not automatically an instance of Timestampable. For that, you need an interface:
<?php
// Trait for the implementation — methods copied into the class
trait TimestampableTrait
{
private ?\DateTime $createdAt = null;
public function touch(): void
{
$this->createdAt ??= new \DateTime();
}
public function getCreatedAt(): ?\DateTime
{
return $this->createdAt;
}
}
// Interface for the contract — type hints
interface TimestampableInterface
{
public function touch(): void;
public function getCreatedAt(): ?\DateTime;
}
// Combine both — the most complete
class Article implements TimestampableInterface
{
use TimestampableTrait; // implementation from the trait
// contract from the interface — no need to rewrite because the trait implements it
}
// Now it can be type-hinted!
function processTimestamp(TimestampableInterface $entity): void
{
$entity->touch();
echo $entity->getCreatedAt()->format('Y-m-d') . "\n";
}
processTimestamp(new Article()); // works because Article implements the interface
When Traits Are Right and When to Avoid Them #
Use a Trait when:
✓ The same logic needs to exist in unrelated classes
✓ The feature is "horizontal" — not an is-a relationship, but has-behavior
✓ The implementation is stable enough that it doesn't need to be swapped/replaced
✓ Real examples: Timestampable, SoftDelete, HasUuid, Observable, Singleton
Avoid a Trait when:
✗ The implementation needs to be swappable (use interface + injection)
✗ The trait heavily depends on properties of the class using it — hidden coupling
✗ The class uses too many traits so it's hard to tell where methods come from
✗ This is actually an is-a relationship — use inheritance
✗ You want to form a type for type hints — use an interface
<?php
// ANTI-PATTERN: a trait depending on class properties without a contract
trait PriceCalculation
{
public function calculateTotal(): float
{
// Assumes $this->price and $this->qty exist in the class — no guarantee!
return $this->price * $this->qty * (1 + $this->tax);
}
}
class Invoice
{
use PriceCalculation;
// How do we know $price, $qty, $tax must exist? No clear documentation
}
// CORRECT: declare the required properties inside the trait itself,
// or use abstract methods to enforce them
trait PriceCalculationV2
{
abstract protected function getPrice(): float;
abstract protected function getQty(): int;
abstract protected function getTax(): float;
public function calculateTotal(): float
{
return $this->getPrice() * $this->getQty() * (1 + $this->getTax());
}
}
Summary #
- Traits are a horizontal reuse mechanism — inserting methods and properties into any class without inheritance. Most useful for capabilities needed by unrelated classes in a hierarchy.
- Traits don’t form a type — a class that
use Timestampabledoesn’t automatically become an instance ofTimestampable. For type hints, combine a trait (implementation) with an interface (contract).- Several traits can be used at once —
use TraitA, TraitB, TraitC;. Traits can also use other traits to build more modular compositions.- Method conflicts are resolved with
insteadof(choose one) andas(alias the “defeated” one).ascan also change a method’s visibility.- Abstract methods in traits force classes using the trait to provide specific implementations — a clean way to make traits depend on clear contracts rather than assumptions about class properties.
- Common trait patterns in frameworks: Singleton, Observable/Event, Timestampable, SoftDelete, HasUuid, and Fluent method chaining.
- Avoid traits when: the implementation needs to be swappable (use an interface), the trait has too many hidden assumptions about the class using it, or this is really an is-a relationship better expressed through inheritance.
- Trait + Interface is the strongest combination: the trait provides the implementation copied into the class, the interface provides a contract usable for type hints and dependency injection.