Als je PHP-code schrijft die na een paar maanden nog steeds prettig werkt, kom je onvermijdelijk de SOLID principes tegen. De SOLID principes zijn vijf ontwerpregels voor objectgeoriënteerde code, bedacht door Robert C. Martin, die helpen bij het bouwen van onderhoudbare en uitbreidbare applicaties.
In dit artikel loop je stap voor stap door alle vijf de principes heen, met praktische PHP-voorbeelden. Geen academische theorie, maar code die je morgen in je eigen project kunt gebruiken.
De principes bouwen voort op object-oriented PHP basics, dus een basiskennis van classes en interfaces is handig.
Wat betekent SOLID precies?
SOLID is een acroniem dat staat voor vijf principes:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Elk principe richt zich op één specifiek probleem in OOP-code: verantwoordelijkheden, uitbreidbaarheid, overervingsgedrag, interfaces en afhankelijkheden. Samen vormen ze een set richtlijnen die je code helpen groeien zonder dat het een onleesbare kluwen wordt.
Belangrijk: SOLID is geen vervanging voor design patterns in PHP, maar een aanvulling. Veel patterns zijn juist een praktische invulling van deze principes.
Single Responsibility Principle (SRP)
"Een klasse zou maar één reden moeten hebben om te veranderen." Dat is de klassieke definitie. In de praktijk betekent het: elke klasse doet één ding, en doet dat goed.
Een slecht voorbeeld
class Order
{
public function calculateTotal(): float { /* ... */ }
public function saveToDatabase(): void { /* ... */ }
public function sendConfirmationEmail(): void { /* ... */ }
public function generatePdfInvoice(): string { /* ... */ }
}
Deze Order-klasse heeft vier verschillende verantwoordelijkheden: rekenen, opslaan, mailen en PDF'en. Verandert de mailprovider? Je moet Order aanpassen. Wijzigt de databasestructuur? Weer Order.
Een betere versie
class Order
{
public function calculateTotal(): float { /* ... */ }
}
class OrderRepository
{
public function save(Order $order): void { /* ... */ }
}
class OrderMailer
{
public function sendConfirmation(Order $order): void { /* ... */ }
}
class InvoiceGenerator
{
public function generatePdf(Order $order): string { /* ... */ }
}
Elke klasse heeft nu één duidelijke taak. Testen wordt eenvoudiger, want je mockt alleen de afhankelijkheid die je daadwerkelijk nodig hebt. Lees ook testing in PHP voor meer hierover.
Open/Closed Principle (OCP)
"Klassen moeten open zijn voor uitbreiding, maar gesloten voor aanpassing." Met andere woorden: nieuw gedrag toevoegen mag geen bestaande klassen breken.
Het probleem
class PaymentProcessor
{
public function process(string $type, float $amount): void
{
if ($type === 'ideal') {
// iDEAL-logica
} elseif ($type === 'creditcard') {
// Creditcard-logica
} elseif ($type === 'paypal') {
// PayPal-logica
}
}
}
Elke nieuwe betaalmethode betekent een aanpassing in deze klasse. Risico op regressies neemt toe bij elke wijziging.
De OCP-oplossing
interface PaymentMethod
{
public function pay(float $amount): void;
}
class IdealPayment implements PaymentMethod
{
public function pay(float $amount): void { /* ... */ }
}
class CreditcardPayment implements PaymentMethod
{
public function pay(float $amount): void { /* ... */ }
}
class PaymentProcessor
{
public function process(PaymentMethod $method, float $amount): void
{
$method->pay($amount);
}
}
Een nieuwe betaalmethode? Je voegt een nieuwe klasse toe, zonder PaymentProcessor aan te raken. Dit is in feite het Strategy pattern in actie.
Liskov Substitution Principle (LSP)
"Subklassen moeten vervangbaar zijn door hun parent class zonder dat de correctheid van het programma in gevaar komt." Klinkt abstract, maar betekent: als een functie een Bird verwacht, moet elke subklasse van Bird correct functioneren.
Een klassieke LSP-schending
class Rectangle
{
public function __construct(
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
{
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width;
}
public function setHeight(float $height): void
{
$this->width = $height;
$this->height = $height;
}
}
Een Square lijkt logisch als subklasse van Rectangle, maar het gedrag verschilt: wanneer je de breedte instelt, wijzigt ook de hoogte. Code die van Rectangle uitgaat werkt onvoorspelbaar met een Square.
Een betere aanpak
Gebruik compositie in plaats van overerving, of maak een gezamenlijke interface Shape met een area()-methode. Een Square is wiskundig wel een rechthoek, maar softwarematig niet altijd.
LSP gaat dus over gedrag, niet alleen over types. Een subklasse mag geen verrassingen bevatten.
Interface Segregation Principle (ISP)
"Klanten moeten niet gedwongen worden om te leunen op interfaces die ze niet gebruiken." Oftewel: maak kleine, gerichte interfaces in plaats van één dikke.
Een te brede interface
interface Worker
{
public function work(): void;
public function eat(): void;
public function sleep(): void;
}
class HumanWorker implements Worker { /* ... */ }
class RobotWorker implements Worker
{
public function work(): void { /* ... */ }
public function eat(): void
{
throw new BadMethodCallException('Robots eten niet.');
}
public function sleep(): void
{
throw new BadMethodCallException('Robots slapen niet.');
}
}
De RobotWorker moet methodes implementeren die niets met een robot te maken hebben. Dat is een geur die duidt op te brede interfaces.
Gesegregeerde interfaces
interface Workable
{
public function work(): void;
}
interface Feedable
{
public function eat(): void;
}
interface Sleepable
{
public function sleep(): void;
}
class HumanWorker implements Workable, Feedable, Sleepable { /* ... */ }
class RobotWorker implements Workable { /* ... */ }
Elke klasse implementeert alleen wat ze nodig heeft. Dit maakt code voorspelbaarder en documenteert intentie.
Dependency Inversion Principle (DIP)
"Afhankelijkheden moeten op abstracties rusten, niet op concrete implementaties." High-level modules mogen niet afhankelijk zijn van low-level details.
Het probleem
class UserService
{
private MySqlUserRepository $repository;
public function __construct()
{
$this->repository = new MySqlUserRepository();
}
public function findUser(int $id): ?User
{
return $this->repository->find($id);
}
}
UserService is hard gekoppeld aan MySqlUserRepository. Wil je testen met een in-memory repository, of later overstappen op PostgreSQL? Grote verbouwing.
De geïnverteerde versie
interface UserRepository
{
public function find(int $id): ?User;
}
class MySqlUserRepository implements UserRepository { /* ... */ }
class UserService
{
public function __construct(
private UserRepository $repository,
) {}
public function findUser(int $id): ?User
{
return $this->repository->find($id);
}
}
UserService kent alleen de interface. Welke implementatie er uiteindelijk in gaat, bepaalt een dependency injection container. Frameworks zoals Laravel en Symfony doen dit automatisch. Zie ook Laravel introductie voor hoe dit in de praktijk werkt.
Voor meer diepgaande informatie over dependency injection is de PHP-FIG documentatie over PSR-11 een aanrader.
SOLID in combinatie met andere patronen
SOLID staat niet op zichzelf. Het combineert uitstekend met andere technieken:
- Repository pattern implementeert DIP voor dataopslag, zoals je terugziet in werken met databases in PHP met PDO.
- Domain-driven design leunt zwaar op SRP en DIP; lees meer in domain-driven design in PHP.
- MVC-architectuur scheidt verantwoordelijkheden op architectuurniveau, een toepassing van SRP op hogere abstractie. Zie MVC pattern in PHP.
Een goed geschreven boek hierover is Clean Architecture van Robert C. Martin, dat SOLID in een bredere architectuurcontext plaatst.
Valkuilen bij het toepassen van SOLID
SOLID klinkt als een heilige graal, maar je kunt er ook in doorslaan:
- Te veel abstracties: elke interface voor elke klasse leidt tot een jungle van bestanden zonder meerwaarde.
- Voorbarige optimalisatie: pas SOLID toe wanneer je daadwerkelijk meerdere implementaties of wijzigingen verwacht.
- Principes als regels: SOLID zijn richtlijnen. Soms is een pragmatische oplossing beter dan een strikt "correcte" architectuur.
- Negeren van de context: een simpel script voor eenmalig gebruik heeft geen repository-interface nodig.
Een goede vuistregel: pas SOLID vooral toe in code die langer dan een paar maanden meegaat, of die door meerdere ontwikkelaars onderhouden wordt.
Hoe begin je met SOLID in bestaande code?
Je hoeft niet meteen je hele codebase om te gooien. Begin klein:
- Identificeer pijnpunten: welke klasse moet je vaak aanpassen voor verschillende redenen? Daar past SRP.
- Kijk naar lange if/else-ketens: kandidaten voor OCP en Strategy pattern.
- Zoek harde
new-aanroepen: meestal een DIP-schending die je met dependency injection oplost. - Bekijk je interfaces: zijn ze te breed? ISP kan helpen ze te splitsen.
- Test-eerst: schrijf een test, en laat het ontwerp van de code zich daaraan aanpassen.
Refactoren richting SOLID is een geleidelijk proces. Elk refactor-momentje is winst.
Veelgestelde vragen
Wat zijn de SOLID principes?
SOLID is een acroniem voor vijf ontwerpprincipes: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation en Dependency Inversion. Samen helpen ze om objectgeoriënteerde code onderhoudbaar, uitbreidbaar en testbaar te maken.
Zijn SOLID principes verplicht in PHP?
Nee, het zijn richtlijnen, geen regels. Maar bij grotere PHP-projecten of frameworks zoals Laravel en Symfony levert het consequent toepassen ervan duidelijk onderhoudbaardere code op.
Wat is het belangrijkste SOLID principe om mee te beginnen?
Het Single Responsibility Principle is vaak het makkelijkst te begrijpen en direct toepasbaar. Zodra je klassen kleiner en gerichter maakt, wordt testen en aanpassen een stuk eenvoudiger.
Hoe verhouden SOLID en design patterns zich tot elkaar?
Design patterns zijn concrete oplossingen voor veelvoorkomende problemen, terwijl SOLID algemene principes beschrijft. Veel patterns zijn in feite een praktische invulling van één of meerdere SOLID principes.
Kan je SOLID overdrijven?
Ja, te strikt toepassen leidt tot onnodige abstracties en complexiteit. Gebruik SOLID als kompas, niet als dogma: pas principes toe waar ze aantoonbaar waarde toevoegen.