Introduction
PHP traits explained: a PHP & Laravel guide for developers reveals one of the most underutilized yet powerful features in modern PHP development. Traits solve a fundamental problem: how do you reuse code across unrelated classes without forcing them into an inheritance hierarchy? If you've ever written the same method in three different classes, or felt inheritance constraints in your design, this article is for you.
What are traits, and why do we need them?
Before traits existed, PHP developers faced a painful choice: use single inheritance (limiting code reuse) or duplicate logic across classes. Traits bridge this gap by providing horizontal code reuse—sharing behavior between classes that have no inheritance relationship.
A trait is a mechanism for code reuse in single inheritance languages. It's a collection of methods that you can include in any class, regardless of that class's position in the inheritance hierarchy. Think of traits as mixins: you're mixing in behavior without creating a parent-child relationship.
Here's a simple example of defining a trait:
trait Loggable
{
public function log($message)
{
echo "[LOG] " . date('Y-m-d H:i:s') . " - " . $message . PHP_EOL;
}
}
class User
{
use Loggable;
}
class PaymentProcessor
{
use Loggable;
}
$user = new User();
$user->log('User created'); // Output: [LOG] 2024-01-15 10:30:45 - User created
$processor = new PaymentProcessor();
$processor->log('Payment processed'); // Output: [LOG] 2024-01-15 10:30:46 - Payment processedBoth User and PaymentProcessor now have logging capability without inheriting from a common parent class. They're unrelated, yet share behavior.
Horizontal reuse without inheritance
The traditional inheritance model forces a hierarchical structure. If you need the same functionality in two unrelated classes, you either duplicate code or create an artificial parent class. Both approaches have downsides.
Traits give us a third option: horizontal reuse. Multiple unrelated classes can use the same trait without creating inheritance debt. This keeps your class hierarchies clean and focused on domain semantics, not implementation convenience.
Consider a real-world scenario from an e-commerce system. You have an Order class, an Invoice class, and a Shipment class. All three need timestamp tracking (created_at, updated_at, deleted_at). None of them should inherit from a common parent—they represent different domain concepts.
trait HasTimestamps
{
protected $createdAt;
protected $updatedAt;
protected $deletedAt;
public function markUpdated()
{
$this->updatedAt = new DateTime();
}
public function softDelete()
{
$this->deletedAt = new DateTime();
}
public function isDeleted()
{
return $this->deletedAt !== null;
}
public function restore()
{
$this->deletedAt = null;
}
}
class Order
{
use HasTimestamps;
private $id;
private $total;
}
class Invoice
{
use HasTimestamps;
private $id;
private $amount;
}
class Shipment
{
use HasTimestamps;
private $id;
private $trackingNumber;
}Each class gains timestamp behavior independently. If you need to modify how timestamps work across your application, you update the trait once.
Laravel's heavy use of traits
Laravel's codebase makes extensive use of traits to add behavior to models and classes. Understanding these patterns helps you write more idiomatic Laravel code.
HasFactory trait
The HasFactory trait is included in every model generated by Laravel's scaffolding. It provides a factory method for testing:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Product extends Model
{
use HasFactory;
}
// In tests:
$product = Product::factory()->create();
$products = Product::factory()->count(10)->create();Without this trait, you'd need to manually wire up factory instantiation or duplicate factory logic across models.
Notifiable trait
Laravel's Notifiable trait adds notification routing capabilities to any class, not just users:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class Customer extends Model
{
use Notifiable;
}
// Send a notification to a customer
$customer->notify(new OrderShipped($order));The Notifiable trait includes methods for routing notifications via email, SMS, Slack, and custom channels. It's a perfect example of horizontal reuse—Admin models, User models, and Customer models might all need notifications, but they don't share an inheritance path.
Other common Laravel traits
You'll encounter traits throughout Laravel:
SoftDeletes—adds soft delete functionalityHasUuids—automatically generates UUID primary keysAuthenticatable—enables authentication on any modelAuthorizable—adds authorization (gates and policies)CastsAttributes—enables custom attribute casting
Each trait represents a feature that multiple models might need, independent of class hierarchy.
Avoid traits with hidden state
This is the most important gotcha: traits can introduce hidden, implicit state that creates maintenance nightmares. A trait might define properties that aren't visible when you glance at the class definition.
Consider this problematic pattern:
trait UserAudit
{
private $auditLog = [];
private $auditEnabled = true;
public function recordAction($action)
{
if ($this->auditEnabled) {
$this->auditLog[] = [
'action' => $action,
'timestamp' => now(),
'user' => auth()->id()
];
}
}
public function disableAudit()
{
$this->auditEnabled = false;
}
public function getAuditLog()
{
return $this->auditLog;
}
}
class User
{
use UserAudit;
public function delete()
{
// What properties does User have? You need to check the trait.
// Is auditEnabled actually false here? Hard to track.
}
}Problems with this approach:
- Hidden state—Reading the
Userclass alone doesn't reveal all its properties - Implicit behavior—A method might silently disable auditing; developers using the class won't know
- Collision risks—If another trait also defines
$auditLog, you get naming conflicts - Testability—Unit testing becomes harder when state lives in traits
A better approach: use composition and dependency injection:
class AuditLog
{
private array $entries = [];
private bool $enabled = true;
public function record($action, $userId)
{
if ($this->enabled) {
$this->entries[] = [
'action' => $action,
'timestamp' => now(),
'user' => $userId
];
}
}
public function disable()
{
$this->enabled = false;
}
public function getEntries()
{
return $this->entries;
}
}
class User
{
private AuditLog $audit;
public function __construct(AuditLog $audit = null)
{
$this->audit = $audit ?? new AuditLog();
}
public function delete()
{
$this->audit->record('user_deleted', auth()->id());
// Logic here
}
public function getAuditLog()
{
return $this->audit->getEntries();
}
}Now state is explicit, and testing is straightforward:
public function testUserDeletionIsAudited()
{
$audit = new AuditLog();
$user = new User($audit);
$user->delete();
$this->assertContains('user_deleted', $audit->getEntries());
}Prefer composition for complex behavior
Traits are excellent for sharing simple, stateless methods or small feature sets. For complex behavior, especially behavior with state or dependencies, composition is cleaner.
A real-world example from a ride-hailing platform: drivers need rating calculations, trip history, document verification, and payment settlement. You might be tempted to create traits for each feature:
// Problematic: trait-heavy approach
trait HasRating
{
private $ratings = [];
// ... many methods
}
trait HasTripHistory
{
private $trips = [];
// ... many methods
}
class Driver
{
use HasRating, HasTripHistory;
// Driver class is now implicit and bloated
}A composition-based approach is clearer:
class Rating
{
private array $ratings = [];
private Driver $driver;
public function __construct(Driver $driver)
{
$this->driver = $driver;
}
public function addRating($value, $feedback)
{
$this->ratings[] = ['value' => $value, 'feedback' => $feedback, 'date' => now()];
}
public function averageRating()
{
return array_sum(array_column($this->ratings, 'value')) / count($this->ratings);
}
}
class TripHistory
{
private array $trips = [];
private Driver $driver;
public function __construct(Driver $driver)
{
$this->driver = $driver;
}
public function addTrip(Trip $trip)
{
$this->trips[] = $trip;
}
public function getTotalDistance()
{
return array_sum(array_map(fn($trip) => $trip->distance, $this->trips));
}
}
class Driver
{
private Rating $rating;
private TripHistory $history;
public function __construct()
{
$this->rating = new Rating($this);
$this->history = new TripHistory($this);
}
public function completeTrip(Trip $trip)
{
$this->history->addTrip($trip);
}
public function getAverageRating()
{
return $this->rating->averageRating();
}
}Benefits of composition:
- Explicitness—Dependencies are visible in the constructor
- Testability—Each component can be unit tested in isolation
- Flexibility—You can swap implementations (e.g., a mock Rating for testing)
- Single responsibility—Each class has one reason to change
Common mistakes
Mixing too many traits
When a class uses five or more traits, you've likely created a god class. Step back and consider composition.
Trait name collisions
If two traits define the same method, PHP will raise an error. You can resolve this with an "insteadof" clause, but it's a code smell:
trait A
{
public function greet() { return 'From A'; }
}
trait B
{
public function greet() { return 'From B'; }
}
class Greeter
{
use A, B {
A::greet insteadof B;
}
}This works, but if you're using "insteadof," reconsider your architecture.
Forgetting that traits can't have constructors
Well, technically they can, but the trait's constructor won't be called automatically. Only the class constructor runs. This encourages state issues:
trait Cache
{
// This won't be called automatically
public function __construct()
{
$this->cache = [];
}
private $cache = []; // Better: initialize here
}Using traits for implementation details instead of cross-cutting concerns
Traits work best when they represent a capability or cross-cutting concern (logging, caching, auditing). Using them for domain logic creates confusion.
Key takeaways
- Traits enable horizontal code reuse across unrelated classes without inheritance chains
- Laravel uses traits extensively for features like factories, notifications, soft deletes, and authentication
- Avoid traits with hidden state—use composition and explicit dependencies for complex behavior
- Keep traits simple and focused—they're best for stateless utility methods and cross-cutting concerns
- Prefer composition when state management or complex logic is involved—it's more testable and maintainable
- Be cautious with multiple traits—too many indicates a design issue; consider breaking concerns into separate classes
- Use traits for framework-level features like Laravel does, but keep application logic in clearly defined classes






Comments
Loading comments…