SOLID principles are five design guidelines that make object-oriented code maintainable, extensible, and testable. They are not rules to be applied mechanically but tools for reasoning about design decisions. I show each principle in PHP with a before/after example and a concrete connection to Magento 2 architecture.
S – Single Responsibility Principle
A class should have one reason to change – one responsibility.
<?php
// BAD - three responsibilities in one class
class OrderProcessor
{
public function process(array $order): void
{
// 1. Business logic
$total = array_sum(array_column($order['items'], 'price'));
// 2. Database access
$this->pdo->prepare('INSERT INTO orders ...')->execute([$total]);
// 3. Sending email
mail($order['email'], 'Order confirmed', "Total: {$total}");
}
}
// GOOD - one responsibility each
class OrderCalculator { public function total(array $order): float { ... } }
class OrderRepository { public function save(Order $order): void { ... } }
class OrderEmailNotifier { public function sendConfirmation(Order $order): void { ... } }
class OrderService
{
public function __construct(
private OrderCalculator $calculator,
private OrderRepository $repository,
private OrderEmailNotifier $notifier
) {}
public function process(array $orderData): void
{
$total = $this->calculator->total($orderData);
$order = new Order($orderData, $total);
$this->repository->save($order);
$this->notifier->sendConfirmation($order);
}
}
O – Open/Closed Principle
Open for extension, closed for modification.
<?php
// BAD - adding a shipping method requires modifying this class
class ShippingCalculator
{
public function calculate(string $method, float $weight): float
{
if ($method === 'flat') return 9.99;
if ($method === 'weight') return $weight * 2.5;
// Adding 'express' requires editing this file
throw new \InvalidArgumentException("Unknown: {$method}");
}
}
// GOOD - add new method by adding a new class, zero modification
interface ShippingStrategyInterface
{
public function calculate(float $weight): float;
}
class FlatRateShipping implements ShippingStrategyInterface { ... }
class WeightBasedShipping implements ShippingStrategyInterface { ... }
class ExpressShipping implements ShippingStrategyInterface { ... } // new - no existing code changed
L – Liskov Substitution Principle
Objects of a subtype must be usable wherever the parent type is expected.
<?php
// BAD - subclass weakens the contract
class Rectangle
{
public function __construct(protected int $width, protected int $height) {}
public function area(): int { return $this->width * $this->height; }
public function setWidth(int $w): void { $this->width = $w; }
public function setHeight(int $h): void { $this->height = $h; }
}
class Square extends Rectangle
{
// Square MUST have equal sides, so it changes setWidth/setHeight behaviour
public function setWidth(int $w): void { $this->width = $this->height = $w; }
public function setHeight(int $h): void { $this->width = $this->height = $h; }
}
// This breaks LSP:
function testRectangle(Rectangle $r): void {
$r->setWidth(5);
$r->setHeight(3);
assert($r->area() === 15); // fails for Square! area = 9
}
// GOOD - separate types with a shared interface
interface ShapeInterface { public function area(): int; }
class Rectangle implements ShapeInterface { ... }
class Square implements ShapeInterface { ... }
I – Interface Segregation Principle
Clients should not be forced to depend on methods they do not use.
<?php
// BAD - fat interface forces implementation of unneeded methods
interface ProductInterface
{
public function getName(): string;
public function getPrice(): float;
public function getWeight(): float;
public function getDownloadUrl(): string; // only for digital products!
public function getShippingClass(): string; // only for physical products!
}
// GOOD - segregated interfaces
interface ProductInterface { public function getName(): string; public function getPrice(): float; }
interface PhysicalProductInterface { public function getWeight(): float; public function getShippingClass(): string; }
interface DigitalProductInterface { public function getDownloadUrl(): string; }
class PhysicalProduct implements ProductInterface, PhysicalProductInterface { ... }
class DigitalProduct implements ProductInterface, DigitalProductInterface { ... }
D – Dependency Inversion Principle
Depend on abstractions, not concretions.
<?php
// BAD - high-level class depends on concrete low-level class
class OrderService
{
private MySQLOrderRepository $repository; // concrete dependency
public function __construct()
{
$this->repository = new MySQLOrderRepository(); // new = tight coupling
}
}
// GOOD - depend on abstraction, inject implementation
class OrderService
{
public function __construct(
private OrderRepositoryInterface $repository // interface = abstraction
) {}
}
// In di.xml - bind interface to implementation
// <preference for="OrderRepositoryInterface" type="MySQLOrderRepository"/>
// In tests - inject mock
$service = new OrderService($this->createMock(OrderRepositoryInterface::class));
SOLID in Magento 2
Magento 2’s architecture embodies all five principles:
- SRP – separate Repository, Model, ResourceModel, Collection classes
- OCP – plugins and observers extend behaviour without modifying core
- LSP – preferences must honour the interface contract of the replaced class
- ISP –
@apiinterfaces are fine-grained (ProductRepositoryInterface vs ProductManagementInterface) - DIP – constructors depend on interfaces; di.xml binds implementations
Summary
SOLID is not a checklist to tick off. It is a vocabulary for discussing design tradeoffs. When a class is hard to test – suspect SRP or DIP violation. When adding a feature requires modifying many existing classes – suspect OCP violation. Internalise the principles as diagnostic tools rather than rules, and your design instincts improve naturally.
