Design patterns in PHP: uitleg met voorbeelden

Leer hoe design patterns in PHP werken. Praktische uitleg over Singleton, Factory, Strategy en Observer met codevoorbeelden voor developers in 2026.

21 juni 20266 min leestijdDoor We Develop Communication

Schrijf je grotere PHP-applicaties en merk je dat dezelfde ontwerpproblemen steeds terugkomen? Dan is het tijd om dieper in design patterns in PHP te duiken. Design patterns zijn bewezen, herbruikbare oplossingen voor problemen die developers wereldwijd al decennia tegenkomen.

Of je nu met een eigen framework werkt of met Laravel of Symfony: patterns zoals Singleton, Factory en Strategy zitten overal. Wie ze herkent en bewust toepast, schrijft code die flexibeler, testbaarder en beter onderhoudbaar is.

In dit artikel leer je wat design patterns zijn, welke categorieën er bestaan en hoe je de belangrijkste patterns in PHP toepast met praktische voorbeelden.

Wat zijn design patterns?

Een design pattern is geen kant-en-klare stuk code dat je kopieert, maar een ontwerpidee. Het beschrijft hoe klassen en objecten samenwerken om een terugkerend probleem op te lossen.

Het concept komt uit het klassieke boek Design Patterns: Elements of Reusable Object-Oriented Software van de zogenaamde "Gang of Four". Daarin werden 23 patterns beschreven, verdeeld over drie categorieën:

  • Creational patterns, draaien om het maken van objecten (Singleton, Factory, Builder)
  • Structural patterns, draaien om de samenstelling van klassen (Adapter, Decorator, Facade)
  • Behavioral patterns, draaien om communicatie tussen objecten (Strategy, Observer, Command)

Om design patterns goed te begrijpen, heb je een solide basis in OOP nodig. Lees zo nodig eerst onze object-oriented PHP basics door.

Waarom design patterns gebruiken in PHP?

PHP is van origine een procedurele taal, maar sinds PHP 5 en zeker sinds PHP 8 is object-oriented programmeren de standaard. Juist in grotere projecten helpen patterns om code schaalbaar te houden.

Een paar concrete voordelen:

  • Gedeelde taal, als je zegt "hier gebruik ik een Strategy", weet elke ervaren developer wat je bedoelt.
  • Minder koppeling, patterns dwingen je vaak interfaces te gebruiken, wat losse koppeling oplevert.
  • Betere testbaarheid, losgekoppelde code is eenvoudiger te testen met tools zoals PHPUnit.
  • Herbruikbaarheid, dezelfde oplossing werkt in meerdere projecten.

Slecht toegepast zijn patterns echter een valkuil. Overengineering maakt code juist complexer. Patterns zijn een middel, geen doel.

Singleton: één instantie, overal beschikbaar

Het Singleton pattern zorgt dat er maar één instantie van een klasse bestaat. Dit pattern wordt vaak gebruikt voor configuratie, logging of een database-connectie.

<?php

final class Config
{
    private static ?Config $instance = null;
    private array $settings = [];

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function set(string $key, mixed $value): void
    {
        $this->settings[$key] = $value;
    }

    public function get(string $key): mixed
    {
        return $this->settings[$key] ?? null;
    }
}

$config = Config::getInstance();
$config->set('debug', true);

Singleton heeft een slechte reputatie omdat het globale state introduceert en unit testing bemoeilijkt. Modern PHP lost dit vaak op met dependency injection containers, zoals in Laravel of Symfony.

Factory: objecten maken zonder new

Het Factory pattern centraliseert het aanmaken van objecten. Handig als de constructie complex is of afhangt van een configuratie.

<?php

interface PaymentGateway
{
    public function charge(int $amountInCents): bool;
}

class StripeGateway implements PaymentGateway
{
    public function charge(int $amountInCents): bool { /* ... */ return true; }
}

class MollieGateway implements PaymentGateway
{
    public function charge(int $amountInCents): bool { /* ... */ return true; }
}

class PaymentGatewayFactory
{
    public static function create(string $provider): PaymentGateway
    {
        return match ($provider) {
            'stripe' => new StripeGateway(),
            'mollie' => new MollieGateway(),
            default => throw new InvalidArgumentException("Onbekende provider: $provider"),
        };
    }
}

$gateway = PaymentGatewayFactory::create('stripe');
$gateway->charge(1999);

De calling code weet niets van de concrete klasse, alleen van de interface. Dat maakt het gemakkelijk om een nieuwe gateway toe te voegen zonder bestaande code aan te passen.

Strategy: gedrag uitwisselbaar maken

Met het Strategy pattern kies je tijdens runtime welk algoritme of welke implementatie je gebruikt. Ideaal als je verschillende berekeningen, validaties of formatters hebt.

<?php

interface ShippingCalculator
{
    public function calculate(float $weight): float;
}

class PostNLStrategy implements ShippingCalculator
{
    public function calculate(float $weight): float
    {
        return $weight < 10 ? 6.95 : 12.50;
    }
}

class DHLStrategy implements ShippingCalculator
{
    public function calculate(float $weight): float
    {
        return $weight * 0.85 + 3.00;
    }
}

class Order
{
    public function __construct(private ShippingCalculator $calculator) {}

    public function shippingCost(float $weight): float
    {
        return $this->calculator->calculate($weight);
    }
}

$order = new Order(new DHLStrategy());
echo $order->shippingCost(4.5);

Omdat Order alleen de interface kent, kun je makkelijk een nieuwe carrier toevoegen zonder de order-klasse te wijzigen. Dit is het Open/Closed principle in actie.

Observer: meld veranderingen aan luisteraars

Het Observer pattern laat objecten reageren op veranderingen in een ander object zonder daar strak aan gekoppeld te zijn. Je ziet dit terug in event systems zoals Laravel Events of Symfony's EventDispatcher.

<?php

interface OrderObserver
{
    public function handle(Order $order): void;
}

class SendConfirmationEmail implements OrderObserver
{
    public function handle(Order $order): void { /* mail versturen */ }
}

class UpdateInventory implements OrderObserver
{
    public function handle(Order $order): void { /* voorraad bijwerken */ }
}

class OrderService
{
    /** @var OrderObserver[] */
    private array $observers = [];

    public function subscribe(OrderObserver $observer): void
    {
        $this->observers[] = $observer;
    }

    public function place(Order $order): void
    {
        foreach ($this->observers as $observer) {
            $observer->handle($order);
        }
    }
}

Zware taken zoals mails versturen kun je beter via een queue afhandelen. Hoe dat werkt lees je in ons artikel over asynchrone taken en queues.

Repository: data-access afgeschermd

Het Repository pattern is geen originele Gang of Four pattern, maar enorm populair in moderne PHP-applicaties. Het verbergt de details van data-opslag achter een interface.

<?php

interface UserRepository
{
    public function findById(int $id): ?User;
    public function save(User $user): void;
}

class PdoUserRepository implements UserRepository
{
    public function __construct(private PDO $pdo) {}

    public function findById(int $id): ?User
    {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        return $row ? User::fromRow($row) : null;
    }

    public function save(User $user): void { /* ... */ }
}

De rest van je applicatie werkt alleen met UserRepository. Wil je later wisselen naar een andere storage? Dan maak je simpelweg een andere implementatie. Hoe je veilig met PDO werkt, lees je in ons artikel over werken met databases in PHP met PDO.

Dependency Injection: het lijm van moderne PHP

Dependency Injection (DI) is strikt genomen een principe, geen pattern, maar het is onmisbaar als je patterns effectief wilt gebruiken. In plaats van afhankelijkheden zelf te creëren, geef je ze mee via de constructor.

<?php

class OrderController
{
    public function __construct(
        private OrderService $orders,
        private Logger $logger,
    ) {}
}

Frameworks zoals Laravel en Symfony lossen deze afhankelijkheden automatisch op via een service container. Dit maakt het inzetten van Strategy, Repository en Observer pas echt prettig. In onze Laravel introductie zie je hoe zo'n container in de praktijk werkt.

Wanneer gebruik je welk pattern?

Patterns zijn geen checklist die je van boven naar beneden afwerkt. Herken eerst het probleem, kies dan een oplossing.

  • Veel if/else-takken op een type? Denk aan Strategy of Factory.
  • Onafhankelijke reacties op een actie? Observer.
  • Complex object stapsgewijs bouwen? Builder.
  • Bestaande API aan moderne interface koppelen? Adapter.
  • Data-access loskoppelen? Repository.

Voor complexe domeinlogica combineer je patterns vaak met domain-driven design in PHP, waarin entities en value objects centraal staan.

Veelvoorkomende valkuilen

Design patterns klinken aantrekkelijk, maar ze kunnen ook schade aanrichten.

  1. Overengineering, niet elk stukje code heeft een Factory of Strategy nodig.
  2. Pattern-jacht, patterns forceren waar een simpele functie volstaat, maakt code onleesbaar.
  3. Verkeerde pattern, een Singleton waar DI beter was, leidt tot globale state.
  4. Onvoldoende tests, patterns helpen bij testbaarheid, maar je moet die tests wel schrijven.

Houd principes als SOLID in gedachten. Een goede introductie vind je in de PHP-documentatie over OOP en de uitgebreide pattern-voorbeelden op Refactoring Guru.

Patterns in frameworks

Zodra je de basis beheerst, zie je patterns overal. Laravel's service container is een Service Locator + DI Container. Eloquent is een Active Record. Middleware is een Chain of Responsibility. Events zijn Observer. Queues leunen op Command.

Symfony gebruikt dezelfde bouwstenen, vaak met andere namen. Door patterns te herkennen lees je framework-broncode sneller en begrijp je waarom bepaalde keuzes gemaakt zijn. Bekijk ook de Gang of Four patterns-referentie voor de historische context.

Conclusie

Design patterns in PHP zijn gereedschap, geen dogma. Leer de klassiekers zoals Singleton, Factory, Strategy en Observer kennen, maar pas ze pas toe als het probleem erom vraagt. Begin klein, gebruik interfaces, zet in op dependency injection en laat tests je ontwerp sturen.

Zo bouw je PHP-applicaties die niet alleen vandaag werken, maar ook over een jaar nog prettig zijn om in door te ontwikkelen.

Veelgestelde vragen

Wat zijn design patterns in PHP?

Design patterns zijn herbruikbare oplossingen voor veelvoorkomende problemen in software-ontwerp. In PHP pas je ze vooral toe binnen object-oriented code om structuur, flexibiliteit en onderhoudbaarheid te verbeteren.

Welke design patterns gebruik je het meest in PHP?

De meest gebruikte patterns in PHP zijn Singleton, Factory, Strategy, Observer, Repository en Dependency Injection. Frameworks zoals Laravel en Symfony leunen zwaar op deze patterns onder de motorkap.

Zijn design patterns verplicht in PHP?

Nee, design patterns zijn geen verplichting. Het zijn richtlijnen die helpen bij terugkerende ontwerpvragen. Gebruik ze alleen als ze een concreet probleem oplossen, niet omdat het kan.

Wat is het verschil tussen een design pattern en een framework?

Een design pattern is een abstract ontwerpidee, terwijl een framework een concrete codebase is. Frameworks zoals Laravel implementeren veel patterns, maar de patterns zelf blijven taalonafhankelijk.

Waar begin ik met leren van design patterns in PHP?

Start met de basis van OOP en bekijk daarna eenvoudige patterns zoals Factory en Strategy. Bouw kleine voorbeelden zelf na om te voelen wanneer een pattern wel of geen waarde toevoegt.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.