PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Zasady SOLID w PHP – teoria przekuta w kod

by Henryk Tews / wtorek, 12 kwietnia 2022 / Opublikowano w PHP

SOLID to zestaw pięciu zasad projektowania obiektowego sformułowanych przez Roberta C. Martina. Każdy je zna z nazwy, niewielu stosuje świadomie. Pokazuję każdą zasadę na konkretnym przykładzie PHP – najpierw kod który ją łamie, potem refaktoring, a przy kilku zasadach odniesienie do Magento 2, które SOLID stosuje konsekwentnie w swojej architekturze.

S – Single Responsibility Principle

Klasa powinna mieć tylko jeden powód do zmiany. Inaczej: każda klasa odpowiada za dokładnie jedną rzecz.

<?php

// NARUSZENIE SRP - klasa robi za dużo
class Order
{
    public function calculateTotal(): float { /* ... */ }
    public function saveToDatabase(): void { /* ... */ }  // persystencja
    public function sendConfirmationEmail(): void { /* ... */ }  // komunikacja
    public function generatePdfInvoice(): string { /* ... */ }  // generowanie dokumentów
    public function validateItems(): array { /* ... */ }  // walidacja
}

// Każda z tych odpowiedzialności to osobny powód do zmiany:
// - zmiana struktury bazy = modyfikacja Order
// - zmiana szablonu emaila = modyfikacja Order
// - zmiana formatu faktury = modyfikacja Order
<?php

declare(strict_types=1);

// SRP - każda klasa ma jedną odpowiedzialność

class Order
{
    // Tylko logika domenowa zamówienia
    public function calculateTotal(): float
    {
        return array_sum(array_map(
            fn(OrderItem $item) => $item->getPrice() * $item->getQty(),
            $this->items
        ));
    }

    public function validateItems(): array
    {
        $errors = [];
        foreach ($this->items as $item) {
            if ($item->getQty() <= 0) {
                $errors[] = "Nieprawidłowa ilość: {$item->getSku()}";
            }
        }
        return $errors;
    }
}

class OrderRepository
{
    // Tylko persystencja
    public function save(Order $order): void { /* ... */ }
    public function getById(int $id): Order { /* ... */ }
}

class OrderEmailNotifier
{
    // Tylko komunikacja email
    public function sendConfirmation(Order $order): void { /* ... */ }
}

class InvoiceGenerator
{
    // Tylko generowanie dokumentów
    public function generatePdf(Order $order): string { /* ... */ }
}

W Magento 2 SRP widać w podziale na modele, resource models, repositories i serwisy. Model Magento\Sales\Model\Order zawiera logikę domenową, ResourceModel\Order obsługuje persystencję, a OrderManagement koordynuje operacje biznesowe.

O – Open/Closed Principle

Klasy powinny być otwarte na rozszerzenie, zamknięte na modyfikację. Dodajesz nowe zachowanie bez zmiany istniejącego kodu.

<?php

// NARUSZENIE OCP - każda nowa metoda dostawy = modyfikacja klasy
class ShippingCalculator
{
    public function calculate(string $method, float $weight): float
    {
        if ($method === 'flat') {
            return 9.99;
        }

        if ($method === 'weight') {
            return $weight * 2.5;
        }

        if ($method === 'free') {  // dołożona po czasie
            return 0.0;
        }

        // Za każdym razem gdy dodajesz metodę wysyłki - modyfikujesz tę klasę
        throw new \InvalidArgumentException("Unknown method: {$method}");
    }
}
<?php

declare(strict_types=1);

// OCP - nowa metoda dostawy = nowa klasa, zero zmian w istniejącym kodzie
interface ShippingStrategyInterface
{
    public function calculate(float $weight): float;
    public function getCode(): string;
}

class FlatRateShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight): float { return 9.99; }
    public function getCode(): string { return 'flat'; }
}

class WeightBasedShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight): float { return $weight * 2.5; }
    public function getCode(): string { return 'weight'; }
}

// Nowa metoda dostawy - zero zmian w ShippingCalculator
class FreeShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight): float { return 0.0; }
    public function getCode(): string { return 'free'; }
}

class ShippingCalculator
{
    /** @var ShippingStrategyInterface[] */
    private array $strategies = [];

    public function addStrategy(ShippingStrategyInterface $strategy): void
    {
        $this->strategies[$strategy->getCode()] = $strategy;
    }

    public function calculate(string $method, float $weight): float
    {
        if (!isset($this->strategies[$method])) {
            throw new \InvalidArgumentException("Unknown method: {$method}");
        }

        return $this->strategies[$method]->calculate($weight);
    }
}

W Magento 2 OCP jest realizowane przez system pluginów i preferences. Możesz rozszerzyć każdą klasę bez jej modyfikacji – to OCP wbudowane w architekturę platformy.

L – Liskov Substitution Principle

Obiekty klas pochodnych muszą być wymienne z obiektami klas bazowych bez zmiany poprawności programu. Podklasa nie może zawęzić kontraktu klasy bazowej.

<?php

// NARUSZENIE LSP - podklasa zmienia zachowanie kontraktu
class Rectangle
{
    protected float $width;
    protected float $height;

    public function setWidth(float $width): void  { $this->width = $width; }
    public function setHeight(float $height): void { $this->height = $height; }
    public function area(): float { return $this->width * $this->height; }
}

class Square extends Rectangle
{
    // Kwadrat nadpisuje settery - narusza kontrakt Rectangle!
    // Kod który działa poprawnie z Rectangle posypie się z Square
    public function setWidth(float $width): void
    {
        $this->width  = $width;
        $this->height = $width; // kwadrat wymusza równe boki
    }

    public function setHeight(float $height): void
    {
        $this->height = $height;
        $this->width  = $height;
    }
}

// LSP naruszone - ten kod zakłada że setWidth i setHeight działają niezależnie
function calculateArea(Rectangle $shape): float
{
    $shape->setWidth(5);
    $shape->setHeight(10);
    return $shape->area(); // Rectangle: 50, Square: 100 - błąd!
}
<?php

declare(strict_types=1);

// LSP - oddziel abstrakcje gdy zachowanie jest różne
interface ShapeInterface
{
    public function area(): float;
}

final class Rectangle implements ShapeInterface
{
    public function __construct(
        private readonly float $width,
        private readonly float $height
    ) {}

    public function area(): float { return $this->width * $this->height; }
}

final class Square implements ShapeInterface
{
    public function __construct(
        private readonly float $side
    ) {}

    public function area(): float { return $this->side ** 2; }
}

// Funkcja korzysta z interfejsu - działa poprawnie z obiema klasami
function printArea(ShapeInterface $shape): void
{
    echo "Pole: " . $shape->area() . PHP_EOL;
}

printArea(new Rectangle(5, 10)); // Pole: 50
printArea(new Square(5));        // Pole: 25 - poprawnie

I – Interface Segregation Principle

Klienty nie powinny być zmuszane do implementacji interfejsów których nie używają. Lepiej mieć wiele małych, wyspecjalizowanych interfejsów niż jeden duży.

<?php

// NARUSZENIE ISP - jeden duży interfejs wymusza implementację wszystkiego
interface ProductInterface
{
    public function getName(): string;
    public function getPrice(): float;
    public function getWeight(): float;      // produkty cyfrowe nie mają wagi
    public function getDimensions(): array;  // produkty cyfrowe nie mają wymiarów
    public function getDownloadUrl(): string; // produkty fizyczne nie mają URL do pobrania
    public function getShippingClass(): string; // produkty cyfrowe nie potrzebują
}

// Produkt cyfrowy musi implementować metody których nie potrzebuje
class DigitalProduct implements ProductInterface
{
    public function getWeight(): float
    {
        return 0.0; // bezsensowna implementacja
    }

    public function getDimensions(): array
    {
        return []; // bezsensowna implementacja
    }

    public function getShippingClass(): string
    {
        throw new \LogicException('Digital products have no shipping class');
    }

    // ...reszta metod
}
<?php

declare(strict_types=1);

// ISP - małe, wyspecjalizowane interfejsy
interface ProductInterface
{
    public function getName(): string;
    public function getPrice(): float;
    public function getSku(): string;
}

interface PhysicalProductInterface extends ProductInterface
{
    public function getWeight(): float;
    public function getDimensions(): array;
    public function getShippingClass(): string;
}

interface DigitalProductInterface extends ProductInterface
{
    public function getDownloadUrl(): string;
    public function getDownloadLimit(): int;
}

interface BundleProductInterface extends ProductInterface
{
    /** @return ProductInterface[] */
    public function getBundleItems(): array;
}

// Każda implementacja dostaje tylko to czego potrzebuje
class SimpleProduct implements PhysicalProductInterface
{
    public function getName(): string { return $this->name; }
    public function getPrice(): float { return $this->price; }
    public function getSku(): string { return $this->sku; }
    public function getWeight(): float { return $this->weight; }
    public function getDimensions(): array { return $this->dimensions; }
    public function getShippingClass(): string { return $this->shippingClass; }
}

class EbookProduct implements DigitalProductInterface
{
    public function getName(): string { return $this->name; }
    public function getPrice(): float { return $this->price; }
    public function getSku(): string { return $this->sku; }
    public function getDownloadUrl(): string { return $this->downloadUrl; }
    public function getDownloadLimit(): int { return $this->downloadLimit; }
    // Brak getWeight(), getDimensions() - nie są potrzebne i nie są wymagane
}

Magento 2 stosuje ISP w swoich Service Contracts. Osobne interfejsy dla odczytu (ProductRepositoryInterface::getById), zapisu (save) i listowania (getList) zamiast jednego monolitycznego interfejsu.

D – Dependency Inversion Principle

Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów – szczegóły powinny zależeć od abstrakcji.

<?php

// NARUSZENIE DIP - zależność od konkretnej implementacji
class OrderService
{
    private MySQLOrderRepository $repository; // konkretna implementacja!
    private SmtpEmailSender $emailSender;     // konkretna implementacja!

    public function __construct()
    {
        // Tworzenie zależności wewnątrz klasy - podwójne naruszenie
        $this->repository  = new MySQLOrderRepository();
        $this->emailSender = new SmtpEmailSender('smtp.example.com');
    }

    public function placeOrder(Order $order): void
    {
        $this->repository->save($order);
        $this->emailSender->send($order->getCustomerEmail(), 'Zamówienie przyjęte');
    }
}

// Problemy:
// - nie można użyć innej bazy danych bez modyfikacji OrderService
// - testy wymagają prawdziwego MySQL i SMTP
// - klasy są silnie sprzężone
<?php

declare(strict_types=1);

// DIP - zależ od abstrakcji, wstrzykuj zależności z zewnątrz
interface OrderRepositoryInterface
{
    public function save(Order $order): void;
    public function getById(int $id): Order;
}

interface EmailSenderInterface
{
    public function send(string $recipient, string $subject, string $body): void;
}

// OrderService zależy tylko od interfejsów
class OrderService
{
    public function __construct(
        private OrderRepositoryInterface $repository, // abstrakcja
        private EmailSenderInterface $emailSender      // abstrakcja
    ) {}

    public function placeOrder(Order $order): void
    {
        $errors = $order->validate();
        if (!empty($errors)) {
            throw new \InvalidArgumentException(implode(', ', $errors));
        }

        $this->repository->save($order);
        $this->emailSender->send(
            $order->getCustomerEmail(),
            'Potwierdzenie zamówienia',
            "Zamówienie #{$order->getId()} zostało przyjęte."
        );
    }
}

// Produkcyjne implementacje
class MySQLOrderRepository implements OrderRepositoryInterface { /* ... */ }
class SmtpEmailSender implements EmailSenderInterface { /* ... */ }

// Testowe implementacje - bez bazy i SMTP
class InMemoryOrderRepository implements OrderRepositoryInterface
{
    private array $orders = [];

    public function save(Order $order): void
    {
        $this->orders[$order->getId()] = $order;
    }

    public function getById(int $id): Order
    {
        return $this->orders[$id] ?? throw new \RuntimeException("Order {$id} not found");
    }
}

class FakeEmailSender implements EmailSenderInterface
{
    public array $sentEmails = [];

    public function send(string $recipient, string $subject, string $body): void
    {
        $this->sentEmails[] = compact('recipient', 'subject', 'body');
    }
}

// Test - zero zależności zewnętrznych
$repository = new InMemoryOrderRepository();
$emailSender = new FakeEmailSender();
$service = new OrderService($repository, $emailSender);

$service->placeOrder($order);
assert(count($emailSender->sentEmails) === 1);

DIP to fundament kontenera DI w Magento 2. Cały system di.xml i ObjectManager to właśnie mechanizm który pozwala programować przeciwko interfejsom a konkretne implementacje wstrzykiwać przez konfigurację – bez jednej linii new w kodzie produkcyjnym.

Podsumowanie

SOLID to nie zestaw zasad które stosujesz „od święta” – to sposób myślenia o kodzie który przekłada się na klasy łatwe do testowania, rozszerzania i utrzymania. SRP wymusza małe, skupione klasy. OCP chroni istniejący kod przed regresją. LSP gwarantuje że podklasy są prawdziwie wymienne. ISP utrzymuje interfejsy małe i spójne. DIP odwraca zależności i umożliwia testowanie w izolacji. Magento 2 stosuje wszystkie pięć zasad konsekwentnie – znajomość SOLID pomaga rozumieć dlaczego architektura platformy wygląda właśnie tak.

About Henryk Tews

Co możesz przeczytać następne

PHP 8.1 w praktyce – enumy po miesiącach, serializacja do bazy, Money value object
Magento 2.4.9 – oficjalne wsparcie PHP 8.5, co się zmieniło i jak migrować
PimCore – CMS + PIM + DAM, klasy obiektów, Data Hub GraphQL, integracja z Magento
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}