1. Hook & Problem Statement
You're building a Laravel application. You have a User model that extends Authenticatable. Everything works great. Then you need a Admin user with additional permissions, so you extend User. Then you need a PremiumUser with special features, so you extend Admin. Then you need a GuestUser that inherits some behavior, so you extend User.
Before you know it, your inheritance tree looks like this:
Authenticatable
└── User
├── Admin
│ └── SuperAdmin
└── PremiumUser
├── VipUser
└── EnterpriseUser
Now, you need to add a Moderator that has some admin features but not all. You start thinking: "Should Moderator extend Admin? But then it gets SuperAdmin methods it shouldn't have. Should I duplicate code? Should I copy-paste the methods?"
You're stuck in the inheritance trap. Your class hierarchy has become a rigid, fragile web of dependencies. A change in User breaks SuperAdmin. Adding a new feature requires refactoring the entire tree. Your code is screaming, but you don't know how to fix it.
This is the problem with inheritance-first thinking.
Inheritance is the most overused and misunderstood tool in object-oriented programming. It's often the first solution developers reach for when they see shared behavior. But real-world applications are rarely best modeled with deep class hierarchies.
There's a better way. It's called Composition over Inheritance, and it's the secret to building flexible, maintainable, and scalable systems.
2. Why This Pattern/Concept Exists
The Problem It Solves
Inheritance creates a "is-a" relationship. A Car is a Vehicle. A PremiumUser is a User. This seems natural—until you realize that many real-world relationships aren't strictly hierarchical. They're behavioral.
Consider: a User might have the behavior of being Notifiable, Authenticatable, Billable, and Loggable. These aren't types of users; they're capabilities a user can have. Forcing these capabilities into an inheritance hierarchy creates the "diamond problem," tight coupling, and fragile base classes.
The Pain That Existed Before
Before the Gang of Four book popularized composition, developers relied heavily on inheritance:
- The Fragile Base Class Problem: A change in a parent class could subtly break all child classes. This made maintenance terrifying.
- The Diamond Problem: Multiple inheritance (in languages that allow it) created ambiguity when two parent classes had the same method.
- Deep Hierarchies: Classes became deeply nested, making code hard to understand and navigate.
- Code Duplication: When behavior didn't fit the hierarchy, developers duplicated code across branches.
- Tight Coupling: Child classes were tightly coupled to their parents. Swapping behavior required re-architecting the hierarchy.
Why Large Applications Need It
As applications grow, requirements change. Today's PremiumUser is tomorrow's EnterpriseUser with a different billing model. Inheritance makes change expensive. Composition makes it cheap.
Composition enables:
- Plug-and-play behavior: Add or remove capabilities without changing the class.
- Horizontal reuse: Share behavior across unrelated classes.
- Testing in isolation: Test behaviors independently.
- Evolution: Change behavior by swapping components, not rewriting hierarchies.
3. Real World Analogy
The Swiss Army Knife vs. A Modular Toolkit
Imagine you're a craftsman. You need various tools: a knife, scissors, a can opener, a screwdriver, a file.
The Inheritance Approach (The Swiss Army Knife):
You buy a single Swiss Army Knife with all tools built in. It's a "Multi-Tool" that inherits from "Knife" and "Scissors" and "Screwdriver."
- Problem: When the scissors break, the entire knife is compromised.
- Problem: You can't replace the scissors with a better version.
- Problem: You can't add a new tool (like a flashlight) without redesigning the knife.
- Problem: If you need two screwdrivers, you need two Swiss Army Knives.
The Composition Approach (The Modular Toolkit):
You buy a tool case (the container) and separate, interchangeable tools (the components):
- A knife blade that fits in a handle
- Scissors that snap into a handle
- A screwdriver bit set that clicks into a driver
A flashlight attachment that connects to the handle
Benefit: Broken scissors? Replace just the scissors.
Benefit: Need a new tool? Add it to the case. Existing tools unchanged.
Benefit: Need two screwdrivers? Buy two bits. Same handle.
Benefit: Each tool has a single, focused responsibility.
Analogy Mapping:
-
Tool Case: Your application class (e.g.,
User) -
Interchangeable Tools: Behaviors and capabilities (e.g.,
Notifiable,Authenticatable) - Handle: The common interface that makes tools compatible
- Attaching a tool: Dependency injection (the class receives the capability)
- Buying a new tool: Adding a new behavior without modifying existing classes
The modular toolkit is more flexible, maintainable, and extensible than the all-in-one Swiss Army Knife.
4. The Pain (Bad Design)
Let's look at a classic inheritance disaster in a Laravel context.
// Base class
class Vehicle
{
protected string $model;
protected int $year;
public function getInfo(): string
{
return "{$this->model} ({$this->year})";
}
public function startEngine(): string
{
return "Engine started";
}
public function honk(): string
{
return "Honk!";
}
}
// Level 1
class Car extends Vehicle
{
protected int $numberOfDoors = 4;
protected bool $hasSunroof = false;
public function lockDoors(): string
{
return "Doors locked";
}
}
// Level 2
class ElectricCar extends Car
{
protected int $batteryCapacity;
public function startEngine(): string
{
// Override to use electric motor
return "Electric motor started silently";
}
public function charge(): string
{
return "Charging battery...";
}
}
// Level 3
class TeslaModel3 extends ElectricCar
{
public function __construct()
{
$this->model = "Tesla Model 3";
$this->year = 2024;
$this->batteryCapacity = 75;
$this->hasSunroof = true;
$this->numberOfDoors = 4;
}
public function autopilot(): string
{
return "Autopilot engaged";
}
}
Now, your boss says: "We need a FlyingCar that can both drive and fly. And a BoatCar for amphibious operations."
// Where does FlyingCar fit? It's a Car? But it flies like a Plane.
// It can't extend both Car and Plane.
class FlyingCar // extends Car? extends Plane? extends both?
{
// We're stuck. Multiple inheritance isn't allowed in PHP.
}
// Solution: Duplicate code?
class FlyingCar extends Car
{
// We have to copy the fly methods from Plane
public function takeOff(): string
{
return "Taking off...";
}
public function land(): string
{
return "Landing...";
}
}
Why This Is Terrible
Tight Coupling: Every class is bound to its parent. Changing
Vehicleaffects every child.Rigidity: Adding new behavior requires modifying the hierarchy.
FlyingCarcan't reusePlanecode.Code Duplication:
FlyingCarduplicates thetakeOffandlandmethods fromPlane.Deep Hierarchies:
TeslaModel3is 4 levels deep. Understanding it requires tracing through all parents.-
Violates SOLID:
-
SRP:
Vehicleis responsible for engine, honking, and all vehicle logic. -
OCP: Adding
FlyingCarrequired creating new subclasses and duplicating code. -
LSP:
ElectricCaroverridesstartEnginebut still works as aCar(okay in this case, but often LSP violations happen).
-
SRP:
Why Developers Write Code Like This
We write inheritance-heavy code because:
- It's taught as the first OOP concept in many tutorials.
- It feels natural ("A Car is a Vehicle").
- It seems like code reuse without extra complexity.
- We don't realize the costs until the hierarchy grows.
- We haven't been introduced to composition.
5. Solution Overview
Composition over Inheritance is a design principle that favors assembling objects with capabilities (composition) over creating deep class hierarchies (inheritance).
Core Idea
Instead of saying "A User is a Notifiable entity," you say "A User has a Notification capability."
Instead of building a rigid tree, you build a flexible "Lego set." Each class is a Lego brick that can be combined with others in various ways.
Main Participants
- The Host/Container: The main class that aggregates capabilities.
- The Components: Small, focused classes that provide specific behaviors.
- The Interfaces: Contracts that define what a component can do.
- The Composition: The act of injecting components into the host.
How Objects Collaborate
User (Host)
├── NotificationTrait (Component)
├── AuthenticationTrait (Component)
└── BillingTrait (Component)
Each component handles one aspect of behavior. The host delegates to its components.
Mental Model
Think of a class as a ship.
- Inheritance (The Mega-Yacht): You build a single, monolithic vessel that does everything. To change anything, you must rebuild the ship.
- Composition (A Fleet of Ships): You have a command ship, a supply ship, a medical ship, and a repair ship. They work together but are independent. Swap out the medical ship for a better one without changing the others.
Benefits
- Flexibility: Add, remove, or swap behaviors independently.
- Reusability: Reuse the same component across completely different classes.
- Maintainability: Components are small, focused, and easy to understand.
- Testability: Test each component in isolation.
Trade-offs
- Boilerplate: You may need more classes and more delegation code.
- Complexity: Deciding how to compose objects requires careful design.
- Learning Curve: Developers must learn to think in terms of capabilities, not hierarchies.
6. UML Diagram
Laravel Service Container Architecture
Diagram Explanation
- User is the host class. It doesn't extend anything. It has capabilities.
- User receives its components through constructor injection.
- User delegates behavior to its components.
- Components are interfaces (contracts) that define capabilities.
- Concrete implementations can be swapped (e.g.,
EmailNotifierorSmsNotifierforNotificationService).
The host class is small, focused, and flexible. The components are reusable across different hosts.
7. Vanilla PHP Example
Let's refactor the Vehicle disaster using composition.
Before Refactoring (The Inheritance Mess)
// Complex inheritance hierarchy as shown above
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class ElectricCar extends Car { /* ... */ }
class TeslaModel3 extends ElectricCar { /* ... */ }
// FlyingCar? BoatCar? We're stuck.
After Refactoring (Composition)
// Step 1: Define interfaces for capabilities
interface Startable
{
public function start(): string;
}
interface Honkable
{
public function honk(): string;
}
interface Flyable
{
public function takeOff(): string;
public function land(): string;
}
interface Floatable
{
public function sail(): string;
}
interface Autopilotable
{
public function autopilot(): string;
}
// Step 2: Implement components (behaviors)
class CombustionEngine implements Startable
{
public function start(): string
{
return "Engine started with a roar!";
}
}
class ElectricMotor implements Startable
{
public function start(): string
{
return "Electric motor started silently...";
}
}
class AirHorn implements Honkable
{
public function honk(): string
{
return "HONK HONK!";
}
}
class Chime implements Honkable
{
public function honk(): string
{
return "Beep beep!";
}
}
class FixedWing implements Flyable
{
public function takeOff(): string
{
return "Lifting off the runway...";
}
public function land(): string
{
return "Approaching runway for landing...";
}
}
class Hull implements Floatable
{
public function sail(): string
{
return "Gliding across the water...";
}
}
class TeslaAutopilot implements Autopilotable
{
public function autopilot(): string
{
return "Navigating autonomously (Tesla AI)...";
}
}
// Step 3: The host class (composition)
class Vehicle
{
private Startable $engine;
private Honkable $horn;
private array $capabilities = [];
public function __construct(
Startable $engine,
Honkable $horn,
array $capabilities = []
) {
$this->engine = $engine;
$this->horn = $horn;
$this->capabilities = $capabilities;
}
public function start(): string
{
return $this->engine->start();
}
public function honk(): string
{
return $this->horn->honk();
}
public function hasCapability(string $capability): bool
{
return isset($this->capabilities[$capability]);
}
public function do(string $action): string
{
if (isset($this->capabilities[$action])) {
return $this->capabilities[$action]->$action();
}
throw new \Exception("Capability {$action} not available");
}
}
// Step 4: Usage (composition in action)
// Create a regular car
$car = new Vehicle(
new CombustionEngine(),
new AirHorn()
);
echo $car->start(); // "Engine started with a roar!"
echo $car->honk(); // "HONK HONK!"
// Create a Tesla (electric car with autopilot)
$tesla = new Vehicle(
new ElectricMotor(),
new Chime(),
['autopilot' => new TeslaAutopilot()]
);
echo $tesla->start(); // "Electric motor started silently..."
echo $tesla->honk(); // "Beep beep!"
echo $tesla->do('autopilot'); // "Navigating autonomously (Tesla AI)..."
// Create a flying car!
$flyingCar = new Vehicle(
new CombustionEngine(),
new AirHorn(),
[
'takeOff' => new FixedWing(),
'land' => new FixedWing(),
'sail' => new Hull()
]
);
echo $flyingCar->do('takeOff'); // "Lifting off the runway..."
echo $flyingCar->do('sail'); // "Gliding across the water..."
What We Improved
-
No Inheritance:
Vehicleextends nothing. It's a container for capabilities. -
Plug-and-Play: Want to add flying? Add
Flyablecomponent. Want to remove it? Don't include it. -
Reusable Components:
ElectricMotorcan be used in cars, boats, or anything that needs an electric start. -
Testable: Test
ElectricMotorwithout needing aVehicle. TestVehiclewith mock components. - Open/Closed: To add a new capability, create a new component and use it. No need to modify existing classes.
-
Liskov Substitution: Any
Startablecomponent can be substituted for another.Vehicledoesn't care which one.
8. Laravel Internal Example
Laravel is built on composition. Let's look at how the framework uses this principle.
Eloquent Models (A Masterclass in Composition)
Eloquent models don't inherit much behavior. Instead, they use traits and components.
namespace Illuminate\Database\Eloquent;
class Model
{
use HasAttributes,
HasEvents,
HasRelationships,
HasTimestamps,
HasGlobalScopes,
HasVisibility;
// The class doesn't do much itself.
// It delegates to traits (composition by another name).
}
Why This Is Elegant:
- Each trait is focused on one concern.
- You can pick and choose which traits to use in your own models.
- Adding a new feature (e.g.,
SoftDeletes) is as simple as adding a trait.
Middleware Pipeline (Composition in Action)
Middleware is a perfect example of composition using the Pipeline pattern.
// Middleware are small, focused components
class Authenticate
{
public function handle($request, $next)
{
if (!auth()->check()) {
return redirect('/login');
}
return $next($request);
}
}
class EnsureEmailIsVerified
{
public function handle($request, $next)
{
if (!auth()->user()->hasVerifiedEmail()) {
return redirect('/verify-email');
}
return $next($request);
}
}
// The Kernel composes these components
protected $middlewareGroups = [
'web' => [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\App\Http\Middleware\EnsureEmailIsVerified::class,
],
];
Why This Is Elegant:
- Each middleware has one job (SRP).
- They can be composed in any order.
- Adding a new middleware doesn't affect others (OCP).
- They can be reused across routes.
The Notifications System
Laravel's notifications are built on composition, not inheritance.
class OrderShipped extends Notification
{
public function via($notifiable): array
{
// Choose channels based on the notifiable object
return ['mail', 'database', 'broadcast'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->greeting('Hello!')
->line('Your order has been shipped.');
}
public function toDatabase($notifiable): array
{
return ['order_id' => $this->order->id];
}
}
Why This Is Elegant:
- You compose the notification from different channels.
- Each
toXxxmethod is a component that handles one channel. - Channels are swappable (switch from mail to SMS without changing the notification logic).
Service Providers (Configuration Composition)
Service providers compose your application's services.
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Compose services
$this->app->bind(NotificationChannel::class, EmailChannel::class);
$this->app->bind(PaymentGateway::class, StripeGateway::class);
}
}
The application is composed from these providers. Adding a new service means adding a new provider, not changing existing ones.
9. Real Laravel Application Example
Let's build a Content Moderation System using composition.
Scenario
Your application needs to moderate user content (comments, posts, images). Moderation includes:
- Profanity filtering
- Spam detection
- Image NSFW detection
- Sentiment analysis
Different types of content need different combinations of these checks.
Implementation
Step 1: Define the Core Interface
// app/Contracts/ModeratorInterface.php
namespace App\Contracts;
interface ModeratorInterface
{
public function moderate(string $content, array $metadata = []): array;
}
Step 2: Create Moderator Components
// app/Moderators/ProfanityFilter.php
namespace App\Moderators;
use App\Contracts\ModeratorInterface;
class ProfanityFilter implements ModeratorInterface
{
private array $badWords = ['damn', 'hell', 'shoot']; // Simplified
public function moderate(string $content, array $metadata = []): array
{
$result = ['blocked' => false, 'reasons' => []];
foreach ($this->badWords as $word) {
if (stripos($content, $word) !== false) {
$result['blocked'] = true;
$result['reasons'][] = "Contains profanity: {$word}";
}
}
return $result;
}
}
// app/Moderators/SpamDetector.php
namespace App\Moderators;
use App\Contracts\ModeratorInterface;
class SpamDetector implements ModeratorInterface
{
public function moderate(string $content, array $metadata = []): array
{
$result = ['blocked' => false, 'reasons' => []];
// Check for spam patterns
if (preg_match('/buy now|click here|free money/i', $content)) {
$result['blocked'] = true;
$result['reasons'][] = 'Contains spam keywords';
}
if (strpos($content, 'http') !== false && count(explode(' ', $content)) < 10) {
$result['blocked'] = true;
$result['reasons'][] = 'Short content with suspicious links';
}
return $result;
}
}
// app/Moderators/SentimentAnalyzer.php
namespace App\Moderators;
use App\Contracts\ModeratorInterface;
class SentimentAnalyzer implements ModeratorInterface
{
public function moderate(string $content, array $metadata = []): array
{
// In a real app, this would call an external API
$sentimentScore = $this->analyzeSentiment($content);
return [
'blocked' => $sentimentScore < -0.8,
'score' => $sentimentScore,
'reasons' => $sentimentScore < -0.8 ? ['Extremely negative sentiment'] : []
];
}
private function analyzeSentiment(string $content): float
{
// Simplified implementation
$positiveWords = ['great', 'awesome', 'beautiful', 'nice'];
$negativeWords = ['terrible', 'awful', 'hate', 'worst'];
$positiveCount = 0;
$negativeCount = 0;
foreach ($positiveWords as $word) {
if (strpos($content, $word) !== false) $positiveCount++;
}
foreach ($negativeWords as $word) {
if (strpos($content, $word) !== false) $negativeCount++;
}
return ($positiveCount - $negativeCount) / max(1, $positiveCount + $negativeCount);
}
}
Step 3: The Composer (Orchestrator)
// app/Services/ModerationService.php
namespace App\Services;
use App\Contracts\ModeratorInterface;
use Illuminate\Support\Collection;
class ModerationService implements ModeratorInterface
{
private Collection $moderators;
public function __construct(array $moderators = [])
{
$this->moderators = collect($moderators);
}
public function addModerator(ModeratorInterface $moderator): self
{
$this->moderators->push($moderator);
return $this;
}
public function moderate(string $content, array $metadata = []): array
{
$results = [];
$blocked = false;
$reasons = [];
foreach ($this->moderators as $moderator) {
$result = $moderator->moderate($content, $metadata);
$results[] = $result;
if ($result['blocked'] ?? false) {
$blocked = true;
$reasons = array_merge($reasons, $result['reasons'] ?? []);
}
}
return [
'blocked' => $blocked,
'reasons' => $reasons,
'details' => $results
];
}
}
Step 4: Service Provider
// app/Providers/ModerationServiceProvider.php
namespace App\Providers;
use App\Contracts\ModeratorInterface;
use App\Moderators\ProfanityFilter;
use App\Moderators\SpamDetector;
use App\Moderators\SentimentAnalyzer;
use App\Services\ModerationService;
use Illuminate\Support\ServiceProvider;
class ModerationServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ModerationService::class, function ($app) {
$service = new ModerationService();
// Compose the service with all moderators
$service->addModerator(new ProfanityFilter());
$service->addModerator(new SpamDetector());
// Only add sentiment analyzer for text content
if (config('moderation.sentiment_enabled')) {
$service->addModerator(new SentimentAnalyzer());
}
return $service;
});
// Or bind the interface to the composed service
$this->app->bind(ModeratorInterface::class, ModerationService::class);
}
}
Step 5: Controller
// app/Http/Controllers/CommentController.php
namespace App\Http\Controllers;
use App\Contracts\ModeratorInterface;
use App\Models\Comment;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function __construct(
private readonly ModeratorInterface $moderator
) {}
public function store(Request $request)
{
$moderationResult = $this->moderator->moderate(
$request->input('content'),
['user_id' => $request->user()->id]
);
if ($moderationResult['blocked']) {
return response()->json([
'error' => 'Comment rejected',
'reasons' => $moderationResult['reasons']
], 422);
}
$comment = Comment::create([
'content' => $request->input('content'),
'user_id' => $request->user()->id,
'moderation_score' => $moderationResult['details'] ?? null
]);
return response()->json($comment, 201);
}
}
Why Composition Wins Here
- Flexibility: Add or remove moderators without changing the core logic.
-
Reusability: Use
ProfanityFilterfor comments, posts, and images. - Testability: Test each moderator independently.
- Configuration: Enable/disable moderators via configuration.
-
Extensibility: Add a new
ImageNsfwDetectorwithout affecting existing code.
10. SOLID Principles Mapping
S - Single Responsibility Principle (SRP)
With composition, each component has a single responsibility:
-
ProfanityFilter: Detects bad words only. -
SpamDetector: Detects spam patterns only. -
SentimentAnalyzer: Analyzes sentiment only.
The ModerationService coordinates, but it doesn't do the actual moderation.
O - Open/Closed Principle (OCP)
The system is open for extension (add new moderators) but closed for modification (the core service never changes). Adding a new ImageNsfwDetector requires zero changes to ModerationService.
L - Liskov Substitution Principle (LSP)
All moderators implement the ModeratorInterface. The ModerationService can use any moderator interchangeably. If you pass a ImageNsfwDetector where a ProfanityFilter is expected, it works.
I - Interface Segregation Principle (ISP)
The ModeratorInterface is focused and minimal. It has exactly one method: moderate(). No class is forced to implement methods it doesn't need.
D - Dependency Inversion Principle (DIP)
High-level ModerationService depends on the ModeratorInterface abstraction, not concrete implementations. Low-level moderators depend on the same abstraction.
11. Trade-offs
Benefits
- Flexibility: Swap components at runtime. Add new components without changing existing code.
- Testability: Test components in isolation. Mock components during host testing.
- Reusability: Use the same component across different contexts.
- Maintainability: Changes are localized to specific components.
- Understanding: Small, focused classes are easier to understand than deep hierarchies.
Costs
- Boilerplate: More classes. More interfaces. More documentation.
- Indirection: To understand what a method does, you may need to trace through several components.
- Overhead: The act of composing objects requires careful design decisions.
- Cognitive Load: Developers must learn to think in terms of capabilities, not "is-a" relationships.
When Is Complexity Justified?
Use composition when:
- The behavior is likely to change or extend.
- The behavior is reusable across unrelated classes.
- The class has multiple, independent concerns.
- You need to test behavior in isolation.
Avoid composition when:
- The relationship is truly hierarchical and stable.
- The class has a single, simple behavior.
- The performance overhead of delegation is significant (rarely a factor in web applications).
12. When NOT To Use It
3 Green Flags (USE COMPOSITION)
-
Multiple Behaviors: A class needs to implement several unrelated behaviors (e.g.,
Notifiable,Authenticatable,Billable). - Changing Requirements: The behavior is likely to change or expand over time.
- Cross-Cutting Concerns: The same behavior is needed across different classes (e.g., logging, caching, validation).
3 Red Flags (USE INHERITANCE)
-
Truly Hierarchical: The relationship is "is-a" and unlikely to change (e.g.,
Dog extends Animal). -
Simple Extension: You're adding a small behavior to a framework class (e.g.,
App\User extends Authenticatable). Extending the framework class is the intended use. - Shared State: The behavior relies heavily on the internal state of the parent class. Composition would require exposing that state.
13. Common Mistakes
1. Choosing Composition When Inheritance Was Simpler
// BAD: Over-engineered composition for a simple problem
class Logger
{
private $formatter;
public function __construct(FormatterInterface $formatter) { ... }
public function log($message) { ... }
}
// The formatter never changes. Inheritance would have been fine.
Problem: You've added complexity where none was needed.
Fix: Use inheritance when the relationship is stable and simple.
2. Forgetting to Delegate
// BAD: Composing but not delegating
class Car
{
private Engine $engine;
public function start()
{
// Forgot to use the engine!
return "Car started";
}
}
Problem: The component isn't used. It's just dead weight.
Fix: Always delegate the behavior to the composed component.
3. Exposing Internal Components
// BAD: Exposing internal components
class User
{
public NotificationService $notifier; // Public property!
public function sendNotification($message)
{
$this->notifier->send($this->email, $message);
}
}
// Outside code can change the notifier
$user->notifier = new BadNotifier(); // Danger!
Problem: You've broken encapsulation. External code can manipulate internal state.
Fix: Make components private. Only expose methods that use them.
4. Interface Over-Design
// BAD: Every component must implement a huge interface
interface Loggable
{
public function logInfo(string $message);
public function logWarning(string $message);
public function logError(string $message);
public function logCritical(string $message);
public function logDebug(string $message); // 5 methods!
}
Problem: You're violating ISP. Classes must implement methods they don't need.
Fix: Use smaller, focused interfaces. For example, LogInfo, LogError, etc.
5. "Trait Abuse"
// BAD: Using traits for everything
class User
{
use Loggable, Notifiable, Authenticatable, Billable, \
Cacheable, Validatable, Searchable, Exportable;
}
Problem: The class is now a dumping ground for behavior. You've just moved the problem from inheritance to traits.
Fix: Use traits only when they provide a single, well-defined behavior. Prefer composition over traits when you need to swap implementations.
14. Frequently Asked Interview Questions
Beginner/Intermediate
Q: What is composition over inheritance?
A: A design principle that favors assembling objects with capabilities (composition) rather than creating deep class hierarchies (inheritance).Q: When would you choose composition over inheritance?
A: When behavior is likely to change, is needed across unrelated classes, or when a class has multiple independent concerns.Q: What's the difference between "is-a" and "has-a" relationships?
A: "Is-a" is inheritance (a Car is a Vehicle). "Has-a" is composition (a Car has an Engine).Q: Can you use composition with inheritance in the same class?
A: Yes! They're not mutually exclusive. Use inheritance for the core "is-a" relationship and composition for additional behaviors.Q: How does Laravel's Eloquent use composition?
A: Eloquent uses traits for specific behaviors likeHasAttributes,HasTimestamps, andSoftDeletes. These are forms of composition.
Senior/Architect
Q: Explain the fragility problem with deep inheritance hierarchies.
A: The fragile base class problem occurs when a change in a parent class breaks child classes. Because the child classes depend on implementation details of the parent, even minor changes can cascade and break multiple classes.Q: How does composition help with Liskov Substitution Principle violations?
A: With inheritance, you must ensure the child class can substitute the parent. Composition avoids this entirely because there is no substitution relationship. You're delegating to interfaces.Q: Compare and contrast traits, interfaces, and composition in PHP.
A: Interfaces define contracts. Traits provide horizontal code reuse. Composition is the act of injecting behavior via dependencies. Interfaces + composition is the most flexible approach. Traits are convenient but can lead to the same problems as multiple inheritance.Q: How would you refactor a large inheritance hierarchy to composition?
A: Identify the behaviors in the hierarchy. Extract each behavior into a separate class. Inject those classes as dependencies. Use interfaces to decouple the host from the components. Delegate behavior in the host.Q: How does the Strategy pattern relate to composition?
A: The Strategy pattern is an implementation of composition. You inject a strategy (component) into a context (host), and the context delegates to the strategy. This allows the strategy to be swapped at runtime.
15. Interactive Practice Challenge
The Requirement
You're building a Document Processing System for a law firm. The system handles different types of documents:
- Contracts: Need versioning, digital signatures, and approval workflows.
- Invoices: Need billing, payment tracking, and tax calculations.
- Reports: Need data aggregation, formatting, and export capabilities.
The current implementation uses inheritance and is becoming a nightmare to maintain.
The Code (POOR DESIGN)
// Base class
class Document
{
protected string $title;
protected string $content;
protected array $metadata;
protected array $history = [];
public function save(): void
{
// Save to database
\DB::table('documents')->insert([
'title' => $this->title,
'content' => $this->content,
'metadata' => json_encode($this->metadata),
'history' => json_encode($this->history),
]);
}
public function addHistory(string $entry): void
{
$this->history[] = ['timestamp' => now(), 'entry' => $entry];
}
}
// Level 1
class Contract extends Document
{
protected string $signatureStatus = 'draft';
protected array $signatures = [];
protected string $version = '1.0';
public function sign(string $name, string $signature): void
{
$this->signatures[] = ['name' => $name, 'signature' => $signature, 'signed_at' => now()];
$this->signatureStatus = 'signed';
$this->addHistory("Signed by {$name}");
}
public function addVersion(string $version): void
{
$this->version = $version;
$this->addHistory("New version: {$version}");
}
}
// Level 2
class EmploymentContract extends Contract
{
protected string $position;
protected float $salary;
protected string $startDate;
public function calculateBenefits(): array
{
// Calculate based on position and salary
return [
'health_insurance' => $this->salary * 0.05,
'retirement' => $this->salary * 0.08,
'vacation_days' => $this->position === 'executive' ? 25 : 15,
];
}
}
class ServiceContract extends Contract
{
protected string $serviceType;
protected array $deliverables;
protected string $paymentTerms;
public function calculateMilestones(): array
{
// Calculate payment milestones based on deliverables
$milestoneAmount = 100000 / count($this->deliverables);
$milestones = [];
foreach ($this->deliverables as $index => $deliverable) {
$milestones[] = [
'deliverable' => $deliverable,
'amount' => $milestoneAmount,
'due_date' => now()->addDays($index * 30),
];
}
return $milestones;
}
}
// Level 3 (More complexity!)
class IndependentContractorContract extends EmploymentContract
{
protected string $agency;
protected float $agencyFee;
protected float $hourlyRate;
public function calculateBenefits(): array
{
// Override: Contractors don't get benefits
return [
'health_insurance' => 0,
'retirement' => 0,
'vacation_days' => 0,
];
}
public function calculateInvoice(): array
{
// Calculate invoice with agency fee
return [
'hours' => 160,
'rate' => $this->hourlyRate,
'total' => $this->hourlyRate * 160,
'agency_fee' => $this->agencyFee,
'payout' => ($this->hourlyRate * 160) - $this->agencyFee,
];
}
}
The Challenge
Now the law firm says:
- "We need to add
NDAdocuments that need a specific approval workflow." - "We need to add
ComplianceReportthat has both report formatting AND contract approval." - "We want to add document encryption capabilities to ALL documents."
The inheritance hierarchy is breaking down. NDA doesn't fit neatly anywhere. ComplianceReport needs behaviors from two different hierarchies.
Your Task: Refactor this system using composition over inheritance.
Questions to Consider
- What behaviors are independent of the document type?
- What interfaces would you define?
- How would you compose a document from these behaviors?
- How would you handle documents that need capabilities from multiple domains?
- How would you test this system?
(We won't provide the solution—refactor this code and experience the flexibility of composition!)
16. Final Mental Model
To keep it simple, memorize these three sentences:
- One-sentence definition: Composition over inheritance is a design principle that builds objects by assembling independent components (has-a) rather than creating deep class hierarchies (is-a).
- One-sentence intuition: Build with Lego bricks, not with a mold. You can rearrange bricks infinitely; you're stuck with the mold forever.
- One-sentence decision rule: If the behavior could be reused across unrelated classes or might change in the future, compose it; if the relationship is truly hierarchical and stable, inherit it.
17. Related Concepts
SOLID Principles
- Open/Closed Principle: Composition enables extension without modification.
- Liskov Substitution: Composition avoids LSP violations entirely.
- Single Responsibility: Components are focused on one behavior.
- Dependency Inversion: High-level hosts depend on abstraction components.
Design Patterns
- Strategy Pattern: Encapsulates interchangeable algorithms via composition.
- Decorator Pattern: Adds behavior by wrapping components.
- Adapter Pattern: Composes to adapt interfaces.
- Composite Pattern: Treats individual objects and compositions uniformly.
- Bridge Pattern: Separates abstraction from implementation via composition.
Laravel Internals
- Traits: Laravel's primary composition mechanism for models.
- Service Providers: Compose application services.
- Middleware: Compose request handling.
- Pipeline: Compose processing steps.
- Notifications: Compose notification channels.
Enterprise Patterns
- Encapsulation: Composition is the ultimate encapsulation tool.
- Dependency Injection: The mechanism to inject composed components.
- Inversion of Control: The framework manages component composition.
Final Thoughts
Inheritance isn't evil. It's a tool—and like any tool, it has its place. But it's overused and often misapplied. The "is-a" relationship is much rarer than developers think.
Most of the time, you're dealing with capabilities, behaviors, and concerns. These are best modeled with composition. You can mix and match capabilities without the fragility of deep hierarchies.
The next time you start typing class Something extends AnotherThing, ask yourself: "Is this truly a hierarchy, or am I just sharing behavior? Could I compose this instead?"
Your future self—and your team—will thank you for the flexibility and maintainability.
Stop creating family trees. Start building toolkits.

Top comments (0)