Single Responsibility Principle (SRP)
The Principle
A class should have only one reason to change. Each class should handle only one piece of functionality.
Violation: OrderProcessor Does Everything
namespace Vendor\Sales\Model;
class OrderProcessor
{
public function process(array $orderData): void
{
// Calculates totals
$total = 0;
foreach ($orderData['items'] as $item) {
$total += $item['price'] * $item['qty'];
}
// Saves to database
$this->saveToDatabase($orderData, $total);
// Sends email
$this->sendConfirmationEmail($orderData['email'], $total);
// Updates inventory
$this->updateInventory($orderData['items']);
}
private function saveToDatabase(array $data, float $total): void { /* ... */ }
private function sendConfirmationEmail(string $email, float $total): void { /* ... */ }
private function updateInventory(array $items): void { /* ... */ }
}
This class has four reasons to change: pricing logic, database schema, email templates, and inventory management.
Correct Implementation
// Responsibility 1: Orchestration
class OrderProcessor
{
public function __construct(
private TotalCalculator $calculator,
private OrderRepository $repository,
private NotificationSender $notifier,
private InventoryManager $inventory
) {}
public function process(OrderInterface $order): void
{
$total = $this->calculator->calculate($order);
$order->setTotal($total);
$this->repository->save($order);
$this->notifier->sendOrderConfirmation($order);
$this->inventory->reserve($order->getItems());
}
}
// Responsibility 2: Pricing only
class TotalCalculator
{
public function calculate(OrderInterface $order): float { /* ... */ }
}
// Responsibility 3: Persistence only
class OrderRepository
{
public function save(OrderInterface $order): void { /* ... */ }
}
Each class now has exactly one reason to change.
Open/Closed Principle (OCP)
The Principle
Software entities should be open for extension but closed for modification. You should be able to add new behavior without changing existing code.
Violation: Switch Statements for Tax Calculation
namespace Vendor\Tax\Model;
class TaxCalculator
{
public function calculate(string $country, float $amount): float
{
match ($country) {
'US' => $amount * 0.08,
'UK' => $amount * 0.20,
'DE' => $amount * 0.19,
default => throw new \Exception('Unknown country'),
};
}
}
Adding a new country requires modifying this class.
Correct Implementation
namespace Vendor\Tax\Api;
interface TaxRateProviderInterface
{
public function getCountryCode(): string;
public function getRate(): float;
}
namespace Vendor\Tax\Model\Rate;
classUSTaxRate implements \Vendor\Tax\Api\TaxRateProviderInterface
{
public function getCountryCode(): string { return 'US'; }
public function getRate(): float { return 0.08; }
}
class UKTaxRate implements \Vendor\Tax\Api\TaxRateProviderInterface
{
public function getCountryCode(): string { return 'UK'; }
public function getRate(): float { return 0.20; }
}
namespace Vendor\Tax\Model;
class TaxCalculator
{
private array $providers = [];
public function __construct(iterable $providers)
{
foreach ($providers as $provider) {
$this->providers[$provider->getCountryCode()] = $provider;
}
}
public function calculate(string $country, float $amount): float
{
if (!isset($this->providers[$country])) {
throw new \InvalidArgumentException("No tax rate for {$country}");
}
return $amount * $this->providers[$country]->getRate();
}
}
Adding France tax: create a new class, register via di.xml. Zero modifications to existing code.
Liskov Substitution Principle (LSP)
The Principle
Objects of a superclass should be replaceable with objects of a subclass without altering the correctness of the program.
Violation: Square Extends Rectangle
class Rectangle
{
protected int $width;
protected int $height;
public function setWidth(int $width): void { $this->width = $width; }
public function setHeight(int $height): void { $this->height = $height; }
public function getArea(): int { return $this->width * $this->height; }
}
class Square extends Rectangle
{
public function setWidth(int $width): void
{
$this->width = $width;
$this->height = $width; // Forces height = width
}
public function setHeight(int $height): void
{
$this->width = $height; // Forces width = height
$this->height = $height;
}
}
// Client code breaks:
function getArea(Rectangle $rect): int
{
$rect->setWidth(5);
$rect->setHeight(4);
return $rect->getArea(); // Expects 20, Square returns 16
}
Correct Implementation
interface ShapeInterface
{
public function getArea(): float;
}
class Rectangle implements ShapeInterface
{
public function __construct(
private float $width,
private float $height
) {}
public function getArea(): float
{
return $this->width * $this->height;
}
}
class Square implements ShapeInterface
{
public function __construct(private float $side) {}
public function getArea(): float
{
return $this->side * $this->side;
}
}
Both implement the same contract without violating expectations.
Interface Segregation Principle (ISP)
The Principle
Clients should not be forced to depend on interfaces they do not use. Prefer many small, specific interfaces over one large, general-purpose interface.
Violation: Fat Widget Interface
interface WidgetInterface
{
public function render(): string;
public function handleForm submission(array $data): void;
public function getCacheKey(): string;
public function getSearchData(): array;
public function export(): string;
}
// A simple display widget must implement methods it doesn't need
class SimpleTextWidget implements WidgetInterface
{
public function render(): string { return '<p>Hello</p>'; }
public function handleFormSubmission(array $data): void {
throw new \BadMethodCallException('Not supported');
}
public function getCacheKey(): string { return md5($this->render()); }
public function getSearchData(): array { return []; }
public function export(): string { return $this->render(); }
}
Correct Implementation
interface RenderableInterface
{
public function render(): string;
}
interface CacheableInterface
{
public function getCacheKey(): string;
}
interface FormAwareInterface
{
public function handleFormSubmission(array $data): void;
}
interface SearchableInterface
{
public function getSearchData(): array;
}
// Simple widget only implements what it needs
class SimpleTextWidget implements RenderableInterface, CacheableInterface
{
public function render(): string { return '<p>Hello</p>'; }
public function getCacheKey(): string { return md5($this->render()); }
}
Each interface has a single purpose. Classes implement only the contracts they actually need.
Dependency Inversion Principle (DIP)
The Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Violation: Direct Dependence on Concrete Class
namespace Vendor\Catalog\Model;
class ProductExporter
{
private CsvFileWriter $writer;
public function __construct()
{
$this->writer = new CsvFileWriter(); // Hard dependency
}
public function export(ProductInterface $product): void
{
$data = $product->toArray();
$this->writer->write($data);
}
}
You cannot switch to XML export without modifying ProductExporter.
Correct Implementation
namespace Vendor\Catalog\Api;
interface ExporterInterface
{
public function export(array $data): void;
}
namespace Vendor\Catalog\Model\Export;
class CsvExporter implements \Vendor\Catalog\Api\ExporterInterface
{
public function __construct(private CsvFileWriter $writer) {}
public function export(array $data): void
{
$this->writer->write($data);
}
}
namespace Vendor\Catalog\Model;
class ProductExporter
{
public function __construct(
private \Vendor\Catalog\Api\ExporterInterface $exporter
) {}
public function export(ProductInterface $product): void
{
$this->exporter->export($product->toArray());
}
}
ProductExporter depends on an abstraction (ExporterInterface). The concrete implementation is injected via DI.
Quiz
1. Which SOLID principle states that a class should have only one reason to change?
2. The Liskov Substitution Principle ensures that:
3. What does the Open/Closed Principle protect against?
4. Interface Segregation Principle recommends:
Flashcards
Question
What does SRP stand for?
Click to reveal answer
Answer
Single Responsibility Principle - one class, one reason to change
Question
What does OCP stand for?
Click to reveal answer
Answer
Open/Closed Principle - open for extension, closed for modification
Question
What does LSP stand for?
Click to reveal answer
Answer
Liskov Substitution Principle - subtypes must be substitutable for base types
Question
What does ISP stand for?
Click to reveal answer
Answer
Interface Segregation Principle - many small interfaces over one large
Question
What does DIP stand for?
Click to reveal answer
Answer
Dependency Inversion Principle - depend on abstractions, not concretions
Revision Notes
Key Takeaways
- 1. SRP: One class = one responsibility = one reason to change
- 2. OCP: Use interfaces/abstractions so new features don't modify existing code
- 3. LSP: Subclasses must honor the contract of the parent class
- 4. ISP: Split fat interfaces into small, role-specific contracts
- 5. DIP: High-level modules depend on abstractions injected via constructor
Interview Tips
- • Give real examples of violating each principle and how to fix it
- • Discuss how SOLID principles map to Magento patterns (DI, interfaces, plugins)
- • Know the trade-offs: over-applying SOLID can lead to excessive abstraction
Cheat Sheet
SRP → One reason to change
OCP → Extend without modify
LSP → Subclasses honor parent contract
ISP → Small, focused interfaces
DIP → Depend on abstractions, inject concretions