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.
