Entwurfsmuster in PHP: Übersicht unabhängig von Magento/Symfony
AI generated
<?php
8.4
PHP 8.4 · Entwurfsmuster · Design Patterns · OOP
Entwurfsmuster in PHP
eine Übersicht unabhängig von Magento und Symfony

Entwurfsmuster werden oft an ein bestimmtes Framework gebunden erklärt, dabei sind es benannte, wiederverwendbare Lösungsformen für Entwurfsprobleme, die in jeder objektorientierten PHP-Codebasis auftauchen, unabhängig davon, ob ein Framework überhaupt im Spiel ist. Dieser Beitrag stellt die wichtigsten klassischen Entwurfsmuster, Factory Method, Singleton, Decorator, Strategy, Observer und Repository, ausschließlich anhand von reinem PHP 8.4 vor, gegliedert nach den drei klassischen GoF-Kategorien Creational, Structural und Behavioral. Kein Magento, kein Symfony, keine ORM-Bindung, nur die Mechanik der Muster selbst und eine ehrliche Einschätzung, wann sich der zusätzliche Code tatsächlich lohnt.

14 Min. Lesezeit Factory · Strategy · Observer · Decorator · Repository PHP 8.4 · framework-unabhängig

1. Was ein Entwurfsmuster wirklich ist

Ein Entwurfsmuster ist eine benannte, wiederverwendbare Lösungsform für ein wiederkehrendes Entwurfsproblem, keine Bibliothek, kein Framework-Feature und kein fertiges Stück Code zum Kopieren. Es beschreibt eine Struktur aus Klassen und ihren Beziehungen zueinander, die ein bestimmtes Entwurfsproblem lösen, ohne an eine konkrete Implementierung, eine bestimmte Programmiersprache oder gar ein bestimmtes Framework gebunden zu sein. Wenn zwei Entwickler von einer "Factory" oder einer "Strategy" sprechen, verständigen sie sich über eine Struktur, nicht über eine bestimmte Codezeile.

Die Design-Patterns-Bewegung wurde durch das Buch der sogenannten "Gang of Four" geprägt, das die Muster in drei Kategorien einteilt: Creational Patterns befassen sich mit der Erzeugung von Objekten, Structural Patterns mit der Zusammensetzung von Klassen und Objekten zu größeren Strukturen, Behavioral Patterns mit der Verteilung von Verantwortung und Kommunikation zwischen Objekten. Diese drei Kategorien dienen als Landkarte für den Rest dieses Beitrags: Jedes vorgestellte Muster wird genau einer dieser Kategorien zugeordnet, damit klar wird, welches Grundproblem es eigentlich löst.

Wichtig ist von Anfang an die Abgrenzung: Ein Entwurfsmuster ist unabhängig von Magento, Symfony oder jedem anderen Framework. Framework-spezifische Implementierungen, etwa wie Magento seinen eigenen ObjectManager oder seine eigenen Factory-Klassen strukturiert, sind lediglich eine konkrete Anwendung des allgemeinen Prinzips auf eine bestimmte Codebasis. Dieser Beitrag bleibt bewusst auf der Ebene des allgemeinen Prinzips, mit reinem PHP-Code, der in jedem Projekt unabhängig vom eingesetzten Framework funktioniert.

2. Creational Patterns Teil 1: Factory Method

Das Problem, das Factory Method löst, entsteht fast immer auf dieselbe Art: Ein new ConcreteClass()-Aufruf ist über eine Codebasis verstreut, und jede dieser Stellen muss geändert werden, sobald sich entscheidet, welche konkrete Klasse tatsächlich instanziiert werden soll. Statt diese Entscheidung an jeder Aufrufstelle zu wiederholen, kapselt eine Factory-Methode die Entscheidungslogik an genau einer Stelle und liefert eine Instanz zurück, die über ein gemeinsames Interface typisiert ist.

Der Aufrufer kennt dabei nur das Interface, nicht die konkrete Klasse. Das entkoppelt den Code, der ein Objekt benutzt, vollständig vom Code, der entscheidet, welche konkrete Implementierung dieses Objekt tatsächlich sein soll.


<?php

declare(strict_types=1);

namespace Notifications;

interface NotifierInterface
{
    public function send(string $message): void;
}

final class EmailNotifier implements NotifierInterface
{
    public function send(string $message): void
    {
        // ... send an email
    }
}

final class SmsNotifier implements NotifierInterface
{
    public function send(string $message): void
    {
        // ... send an SMS
    }
}

// Factory Method: one place decides which concrete class is returned
final class NotifierFactory
{
    public static function create(string $channel): NotifierInterface
    {
        return match ($channel) {
            'email' => new EmailNotifier(),
            'sms' => new SmsNotifier(),
            default => throw new \InvalidArgumentException("Unknown channel: $channel"),
        };
    }
}

$notifier = NotifierFactory::create('email');
$notifier->send('Order confirmed');

Neue Kanäle lassen sich hinzufügen, ohne den aufrufenden Code an irgendeiner Stelle anzufassen, solange nur die Factory-Methode selbst um einen neuen match-Zweig erweitert wird. Das ist der eigentliche Nutzen von Factory Method: Änderungen an der Erzeugungslogik bleiben lokal, statt sich über die gesamte Codebasis zu verteilen.

3. Creational Patterns Teil 2: Singleton und seine Grenzen

Singleton verspricht, dass genau eine Instanz einer Klasse während der gesamten Laufzeit eines Programms existiert, zugänglich über einen globalen statischen Zugriffspunkt. In der Praxis hat sich dieses Muster jedoch zu einem der am häufigsten kritisierten Entwurfsmuster überhaupt entwickelt, weil sein tatsächlicher Preis den versprochenen Nutzen meist übersteigt.

Der Preis liegt in globalem, mutierbarem Zustand: Jede Klasse, die intern auf ein Singleton zugreift, hat eine versteckte Abhängigkeit, die aus ihrer öffentlichen Signatur nicht ersichtlich ist. Das erschwert Tests erheblich, weil ein Singleton zwischen Testfällen zurückgesetzt werden muss, um Seiteneffekte zu vermeiden, und macht den tatsächlichen Abhängigkeitsgraphen einer Anwendung schwerer nachvollziehbar. Die moderne Alternative ist Dependency Injection: Eine Instanz wird einmal zentral erzeugt und explizit über den Konstruktor an jede Stelle weitergereicht, die sie benötigt, statt sich über einen globalen Zugriffspunkt selbst zu beschaffen. Die Instanz bleibt dabei durchaus einzigartig, nur die Art, wie Code an sie herankommt, ändert sich von implizit-global zu explizit-injiziert, was Testbarkeit und Nachvollziehbarkeit deutlich verbessert.

4. Structural Patterns: Decorator

Decorator löst das Problem, ein Objekt um zusätzliches Verhalten zu erweitern, ohne dessen Klasse selbst zu verändern und ohne für jede denkbare Kombination von Zusatzverhalten eine eigene Unterklasse zu schreiben. Ein Decorator implementiert dasselbe Interface wie das Objekt, das er umschließt, hält intern eine Referenz auf dieses Objekt und ruft dessen Methode auf, ergänzt um eigenes Verhalten davor oder danach.

Der entscheidende Vorteil gegenüber Vererbung: Mehrere Decorators lassen sich beliebig verschachteln, jeder fügt genau eine zusätzliche Fähigkeit hinzu, ohne dass eine kombinatorische Explosion von Unterklassen für jede mögliche Kombination entstehen müsste.


<?php

declare(strict_types=1);

namespace Notifications;

interface NotifierInterface
{
    public function send(string $message): void;
}

final class BaseNotifier implements NotifierInterface
{
    public function send(string $message): void
    {
        echo "Sending: {$message}\n";
    }
}

// Decorator: wraps any NotifierInterface, adds behavior without modifying it
final class LoggingNotifierDecorator implements NotifierInterface
{
    public function __construct(
        private readonly NotifierInterface $inner,
    ) {
    }

    public function send(string $message): void
    {
        error_log(sprintf('Sending notification: %s', $message));
        $this->inner->send($message);
    }
}

final class RateLimitedNotifierDecorator implements NotifierInterface
{
    private int $sentCount = 0;

    public function __construct(
        private readonly NotifierInterface $inner,
        private readonly int $maxPerRequest = 5,
    ) {
    }

    public function send(string $message): void
    {
        if ($this->sentCount >= $this->maxPerRequest) {
            throw new \RuntimeException('Rate limit exceeded for this request');
        }

        $this->sentCount++;
        $this->inner->send($message);
    }
}

// Stacking decorators: each one adds exactly one capability
$notifier = new RateLimitedNotifierDecorator(
    new LoggingNotifierDecorator(new BaseNotifier())
);
$notifier->send('Order confirmed');

Der aufrufende Code kennt weiterhin nur NotifierInterface und muss nichts über die konkrete Verschachtelung wissen. Jeder Decorator lässt sich unabhängig testen, und neue Zusatzfähigkeiten kommen als neuer Decorator hinzu, ohne bestehenden Code zu verändern.

5. Behavioral Patterns Teil 1: Strategy

Strategy kapselt austauschbare Algorithmen hinter einem gemeinsamen Interface und wählt zur Laufzeit aus, welcher konkrete Algorithmus tatsächlich verwendet wird. Statt eine große if/match-Kaskade innerhalb einer Klasse zu pflegen, die für jede Variante eine eigene Verzweigung enthält, wird jede Variante als eigene Klasse implementiert und der Kontext-Klasse von außen übergeben.

Neue Varianten lassen sich hinzufügen, ohne die Kontext-Klasse selbst zu verändern, was dem Open/Closed-Prinzip entspricht: offen für Erweiterung, geschlossen für Modifikation.


<?php

declare(strict_types=1);

namespace Pricing;

interface PricingStrategyInterface
{
    public function calculate(int $baseAmountInCents): int;
}

final class RegularPricing implements PricingStrategyInterface
{
    public function calculate(int $baseAmountInCents): int
    {
        return $baseAmountInCents;
    }
}

final class PercentageDiscountPricing implements PricingStrategyInterface
{
    public function __construct(private readonly int $percentOff)
    {
    }

    public function calculate(int $baseAmountInCents): int
    {
        return (int) round($baseAmountInCents * (100 - $this->percentOff) / 100);
    }
}

// Context: holds a strategy, delegates the actual calculation
final class PriceCalculator
{
    public function __construct(
        private readonly PricingStrategyInterface $strategy,
    ) {
    }

    public function priceFor(int $baseAmountInCents): int
    {
        return $this->strategy->calculate($baseAmountInCents);
    }
}

$calculator = new PriceCalculator(new PercentageDiscountPricing(20));
echo $calculator->priceFor(10000); // 8000

6. Behavioral Patterns Teil 2: Observer

Observer entkoppelt eine Ereignisquelle von den Codeteilen, die auf ein Ereignis reagieren müssen, ohne dass die Quelle irgendetwas über die konkreten Beobachter wissen muss. Ein Subject hält eine Liste von Observern und benachrichtigt bei einem Ereignis alle registrierten Observer über ein gemeinsames Interface, ohne deren konkrete Klasse zu kennen.

Dieses Muster lässt sich vollständig mit einem einfachen, selbst definierten Interface umsetzen, ganz ohne die eingebauten SPL-Klassen SplSubject und SplObserver, die in der Praxis selten flexibel genug für reale Anforderungen sind.


<?php

declare(strict_types=1);

namespace Orders;

interface OrderObserverInterface
{
    public function onOrderPlaced(string $orderId): void;
}

final class OrderPlacementLogger implements OrderObserverInterface
{
    public function onOrderPlaced(string $orderId): void
    {
        error_log("Order placed: {$orderId}");
    }
}

final class OrderConfirmationMailer implements OrderObserverInterface
{
    public function onOrderPlaced(string $orderId): void
    {
        // ... send confirmation email
    }
}

// Subject: holds observers, notifies them without knowing their concrete class
final class OrderSubject
{
    /** @var OrderObserverInterface[] */
    private array $observers = [];

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

    public function placeOrder(string $orderId): void
    {
        // ... actual order placement logic

        foreach ($this->observers as $observer) {
            $observer->onOrderPlaced($orderId);
        }
    }
}

$subject = new OrderSubject();
$subject->subscribe(new OrderPlacementLogger());
$subject->subscribe(new OrderConfirmationMailer());
$subject->placeOrder('ORD-1001');

7. Repository als Architektur-Grenze

Repository unterscheidet sich von den vorherigen Mustern dadurch, dass es weniger eine einzelne Klassenbeziehung als vielmehr eine Architektur-Grenze beschreibt. Der Datenzugriff wird hinter einem Interface abstrahiert, sodass die eigentliche Geschäftslogik nicht davon abhängt, ob Daten aus einer Datenbank, einer Datei oder einem In-Memory-Speicher stammen. Business-Code programmiert ausschließlich gegen das Interface und weiß nichts über den konkreten Speichermechanismus dahinter.

Das folgende Beispiel zeigt bewusst eine In-Memory-Implementierung, ausdrücklich ohne jede Bindung an ein ORM oder ein Framework, um die reine Struktur des Musters sichtbar zu machen.


<?php

declare(strict_types=1);

namespace Users;

final class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $email,
    ) {
    }
}

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

// Concrete implementation, plain PHP, no ORM, no framework
final class InMemoryUserRepository implements UserRepositoryInterface
{
    /** @var array<int, User> */
    private array $users = [];

    public function find(int $id): ?User
    {
        return $this->users[$id] ?? null;
    }

    public function save(User $user): void
    {
        $this->users[$user->id] = $user;
    }
}

// Business logic depends only on the interface
final class RegisterUser
{
    public function __construct(
        private readonly UserRepositoryInterface $repository,
    ) {
    }

    public function handle(int $id, string $email): void
    {
        $this->repository->save(new User($id, $email));
    }
}

Ein anderer Speichermechanismus, etwa eine echte Datenbankanbindung, lässt sich später als weitere Implementierung von UserRepositoryInterface ergänzen, ohne dass RegisterUser eine einzige Zeile ändern müsste. Genau diese Austauschbarkeit macht Repository zu einem der am häufigsten eingesetzten Entwurfsmuster in wachsenden Codebasen.

8. Muster kombinieren, ohne zu überengineeren

Entwurfsmuster lösen Probleme, die zuerst tatsächlich existieren müssen, bevor der zusätzliche Code, den ein Muster mitbringt, sich lohnt. Wer eine Factory für eine einzige konkrete Klasse baut, die niemals eine zweite Implementierung bekommen wird, fügt Indirektion hinzu, ohne dass ihr ein realer Nutzen gegenübersteht. Dasselbe gilt für Strategy mit genau einer Strategie oder Observer mit genau einem Observer, der niemals einen zweiten bekommen wird.

Ein brauchbares Warnsignal für Überengineering: Wenn eine Abstraktion existiert, aber seit ihrer Einführung nie eine zweite konkrete Implementierung dazugekommen ist, und auch keine konkret geplant ist, hat die Abstraktion wahrscheinlich mehr Komplexität hinzugefügt, als sie eingespart hat. Ein einfacher, direkter Funktionsaufruf oder eine einzelne Klasse ohne Interface ist in diesem Fall oft die klarere Lösung, und ein Entwurfsmuster lässt sich später immer noch einführen, sobald der tatsächliche Bedarf an Austauschbarkeit entsteht.

9. Entwurfsmuster im direkten Vergleich

Die folgende Tabelle fasst die in diesem Beitrag behandelten Entwurfsmuster entlang von Kategorie, gelöstem Problem und einem typischen Warnsignal für Überengineering zusammen.

Muster Kategorie Gelöstes Problem Warnsignal für Überengineering
Factory Method Creational Erzeugungslogik zentralisieren statt verstreuen Nur eine konkrete Klasse existiert jemals
Singleton Creational Genau eine Instanz global verfügbar machen Fast immer, wegen versteckter globaler Abhängigkeiten
Decorator Structural Verhalten ergänzen ohne Vererbungsexplosion Nur eine einzige Zusatzfähigkeit wird je gebraucht
Strategy Behavioral Austauschbare Algorithmen zur Laufzeit wählen Nur eine Variante existiert und wird es bleiben
Repository Structural / Architektur Datenzugriff von Geschäftslogik entkoppeln Nie mehr als eine Speicherimplementierung geplant

Die Tabelle macht sichtbar, dass jedes dieser Entwurfsmuster ein spezifisches Problem löst, und dass jedes auch ein spezifisches Warnsignal hat, an dem sich Überengineering erkennen lässt. Wer ein Muster einsetzt, ohne dass das zugehörige Problem in der eigenen Codebasis tatsächlich vorliegt, tauscht Einfachheit gegen Indirektion, ohne dafür einen Gegenwert zu erhalten.

10. Zusammenfassung

Entwurfsmuster sind benannte Lösungsformen für wiederkehrende Entwurfsprobleme, unabhängig von Magento, Symfony oder jedem anderen Framework. Factory Method zentralisiert Erzeugungslogik, Decorator ergänzt Verhalten ohne Vererbungsexplosion, Strategy macht Algorithmen zur Laufzeit austauschbar, Observer entkoppelt Ereignisquellen von ihren Reaktionen, Repository trennt Geschäftslogik von der konkreten Speicherimplementierung. Singleton dagegen gehört in den meisten modernen Codebasen eher zu den Mustern, die man bewusst vermeidet, weil sein tatsächlicher Preis, versteckte globale Abhängigkeiten und erschwerte Tests, den Nutzen meist übersteigt.

Der wichtigste Leitsatz bleibt: Ein Muster lohnt sich erst, wenn das Problem, das es löst, tatsächlich in der eigenen Codebasis existiert. Wo keine zweite Implementierung, keine austauschbare Strategie und kein realer Bedarf an Entkopplung besteht, fügt ein Entwurfsmuster nur zusätzliche Indirektion hinzu, ohne einen Gegenwert zu liefern.

Entwurfsmuster in PHP, Das Wichtigste auf einen Blick

Drei GoF-Kategorien

Creational (Erzeugung), Structural (Zusammensetzung), Behavioral (Verantwortung und Kommunikation).

Framework-unabhängig

Muster sind allgemeine Prinzipien. Magento- oder Symfony-Implementierungen sind nur konkrete Anwendungen davon.

Singleton mit Vorsicht

Globaler Zustand und versteckte Abhängigkeiten. Dependency Injection ist meist die robustere Alternative.

Warnsignal Überengineering

Eine Abstraktion ohne zweite Implementierung und ohne konkreten Bedarf kostet mehr, als sie einspart.

11. FAQ: Entwurfsmuster in PHP

1Was ist ein Entwurfsmuster in PHP?
Eine benannte, wiederverwendbare Lösungsform für ein wiederkehrendes Entwurfsproblem. Keine Bibliothek, kein Framework-Feature, sondern eine allgemeine Klassenstruktur.
2Drei Kategorien von Entwurfsmustern?
Creational (Erzeugung), Structural (Zusammensetzung), Behavioral (Verantwortung und Kommunikation zwischen Objekten).
3Warum gilt Singleton als Anti-Pattern?
Globaler mutierbarer Zustand und versteckte Abhängigkeiten erschweren Tests. Dependency Injection ist meist die robustere Alternative.
4Factory Method vs. Strategy?
Factory Method entscheidet, welche Klasse instanziiert wird. Strategy kapselt austauschbare Algorithmen für eine bereits existierende Instanz.
5Wie funktioniert Decorator ohne Vererbung?
Implementiert dasselbe Interface, hält eine Referenz auf das umschlossene Objekt und ergänzt Verhalten. Mehrere Decorators sind beliebig verschachtelbar.
6SplObserver für Observer nötig?
Nein. Ein einfaches eigenes Interface reicht meist und ist flexibler als die eingebauten SPL-Klassen.
7Ist Repository an ein ORM gebunden?
Nein. Ein Interface abstrahiert den Datenzugriff, die konkrete Implementierung kann beliebig sein, ohne dass Business-Code das wissen muss.
8Wann lohnt sich ein Muster nicht?
Wenn das zugehörige Problem gar nicht existiert. Dann fügt das Muster nur Indirektion hinzu, ohne Nutzen zu liefern.
9An Magento oder Symfony gebunden?
Nein. Muster sind allgemeine OOP-Prinzipien. Framework-Implementierungen sind nur konkrete Anwendungen davon.
10Überengineering erkennen?
Eine Abstraktion ohne zweite Implementierung und ohne konkreten Bedarf hat wahrscheinlich mehr Komplexität hinzugefügt, als sie einspart.

Mironsoft

PHP-Entwicklung, Architektur-Review und Entwurfsmuster-Beratung

Entwurfsmuster gezielt in eurem PHP-Projekt einsetzen?

Wir helfen Teams, Factory, Strategy, Decorator, Observer und Repository dort einzusetzen, wo sie tatsächlich Nutzen bringen, und Überengineering gezielt zu vermeiden.

Architektur-Review

Bestehenden Code auf Überengineering und fehlende Entkopplung prüfen

Refactoring

Globale Zustände und Singletons durch Dependency Injection ersetzen

Entwurfs-Workshop

Passende Entwurfsmuster direkt am eigenen Domänenmodell erarbeiten