Skip to content
intermediate Phase 8 · SOLID & Design Principles

SOLID Design Principles

Deep dive into all five SOLID principles with PHP code examples showing violations and correct implementations

1h
0 problems
Topic Progress 0%

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?

Question 1 options

2. The Liskov Substitution Principle ensures that:

Question 2 options

3. What does the Open/Closed Principle protect against?

Question 3 options

4. Interface Segregation Principle recommends:

Question 4 options

Flashcards

Question

What does SRP stand for?

Answer

Single Responsibility Principle - one class, one reason to change

Question

What does OCP stand for?

Answer

Open/Closed Principle - open for extension, closed for modification

Question

What does LSP stand for?

Answer

Liskov Substitution Principle - subtypes must be substitutable for base types

Question

What does ISP stand for?

Answer

Interface Segregation Principle - many small interfaces over one large

Question

What does DIP stand for?

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