Skip OOP Basics?
Are you comfortable with all of the following?
- Classes, objects, and constructors
- Public, protected, and private visibility
- Interfaces vs abstract classes
- Type hinting and return types
- Late static binding
OOP Intermediate — LSP, ISP & Composition
This stage covers the remaining two SOLID principles — Liskov Substitution and Interface Segregation — plus the crucial concept of Composition over Inheritance. These ideas come up repeatedly in the design patterns ahead.
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
Violation
// ❌ FixedDepositAccount breaks the contract — withdraw() throws unexpectedly
class BankAccount {
protected float $balance = 0;
public function withdraw(float $amount): float {
$this->balance -= $amount;
return $this->balance;
}
}
class FixedDepositAccount extends BankAccount {
public function withdraw(float $amount): float {
throw new \RuntimeException("Cannot withdraw from fixed deposit!");
}
}
Any code that expects a BankAccount and calls withdraw() will break when given a FixedDepositAccount. The subclass changed the expected behavior.
Fix
Separate the interface — not all accounts are withdrawable:
interface DepositableInterface {
public function deposit(float $amount): void;
}
interface WithdrawableInterface {
public function withdraw(float $amount): float;
}
class SavingsAccount implements DepositableInterface, WithdrawableInterface { /* ... */ }
class FixedDepositAccount implements DepositableInterface { /* ... */ }
Now code that needs withdrawal only accepts WithdrawableInterface, and FixedDepositAccount is never put in a position where it has to violate expectations.
I — Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they don't use.
Violation
// ❌ A receipt printer doesn't need refund() or getBalance()
interface PaymentInterface {
public function charge(float $amount): bool;
public function refund(float $amount): bool;
public function getBalance(): float;
public function printReceipt(): string;
}
A class that only needs to print receipts is forced to implement charge(), refund(), and getBalance() — methods it will never use. This leads to empty or throwing stub methods that violate LSP.
Fix
// ✅ Smaller, focused interfaces
interface ChargeableInterface {
public function charge(float $amount): bool;
}
interface RefundableInterface {
public function refund(float $amount): bool;
}
interface ReceiptPrintableInterface {
public function printReceipt(): string;
}
A class implements only the interfaces it actually needs. A payment terminal might implement all three; a receipt printer implements only ReceiptPrintableInterface.
How ISP and LSP Work Together
Notice a pattern? When you have a fat interface, classes are forced to implement methods they can't meaningfully support — which leads to LSP violations (throwing exceptions from methods that callers expect to work). ISP prevents LSP violations by making interfaces small enough that every implementor can genuinely fulfill the contract.
Composition Over Inheritance
Inheritance says "is a." Composition says "has a." Composition is usually more flexible.
The Inheritance Problem
// Start simple...
class Logger { public function log($msg) { echo $msg; } }
class FileLogger extends Logger { /* writes to file */ }
class TimestampedLogger extends Logger { /* adds timestamps */ }
// Now you need a timestamped file logger...
class TimestampedFileLogger extends FileLogger { /* duplicate timestamp logic */ }
// And a timestamped console logger...
class TimestampedConsoleLogger extends ConsoleLogger { /* duplicate again! */ }
This is called a class explosion — every combination needs a new class.
The Composition Fix
interface WriterInterface {
public function write(string $message): void;
}
interface FormatterInterface {
public function format(string $message): string;
}
class Logger {
public function __construct(
private WriterInterface $writer,
private FormatterInterface $formatter,
) {}
public function log(string $message): void {
$this->writer->write($this->formatter->format($message));
}
}
Now any writer and any formatter can be combined freely — no subclassing needed. Need a timestamped file logger? Just compose: new Logger(new FileWriter(), new TimestampFormatter()).
This principle will come up repeatedly in the design patterns ahead, especially Decorator, Strategy, and Adapter.
What You Learned
- LSP means subtypes must honor the contract of their parent — if a subclass can't do what the base type promises, the hierarchy is wrong
- ISP means keeping interfaces small and focused — don't force classes to implement methods they can't meaningfully support
- Composition over inheritance avoids class explosion by combining small, focused objects instead of deep hierarchies
- ISP prevents LSP violations: small interfaces mean every implementor can genuinely fulfill the contract
LSP, ISP & Composition in Laravel
LSP — Contracts
Laravel's contracts (Illuminate\Contracts\*) guarantee that implementations are substitutable. Whether you use RedisCache, FileCache, or ArrayCache, any code that depends on Illuminate\Contracts\Cache\Store works the same way:
function warmCache(Store $cache): void {
$cache->put('key', 'value', 3600);
// Works regardless of the underlying driver
}
No cache driver throws an exception for put() or get() — every implementation honors the same contract. That's LSP in action.
ISP — Focused Contracts
Laravel splits its contracts into focused interfaces rather than one massive interface:
Illuminate\Contracts\Auth\Authenticatable— what a user isIlluminate\Contracts\Auth\CanResetPassword— optional capabilityIlluminate\Contracts\Auth\MustVerifyEmail— optional capability
Your User model only implements the interfaces it needs. A user that doesn't need email verification simply doesn't implement MustVerifyEmail — no stub methods required.
Similarly, the queue system has separate interfaces:
ShouldQueue— this job can be queuedShouldBeUnique— this job should not be duplicatedShouldBeEncrypted— this job's payload should be encrypted
Each job opts into only the capabilities it needs.
Composition in Laravel
Laravel heavily favors composition:
- Middleware is composed into a pipeline (not inherited)
- Eloquent scopes add query behavior via composition
- Notification channels are composed (
toMail(),toSlack(),toSms()) - Jobs and Listeners are independent classes composed into the event/queue system
You'll see this pattern everywhere: small, focused pieces snapped together rather than deep inheritance hierarchies.
This example is pre-filled and runnable. Click "Run" to see the output.
Complete the code below to pass all test cases.