PHP und Dependency Injection (DI) - Teil 1

Dependency Injection (DI) ist ein Entwurfsmuster in der Software-Entwicklung, das in vielen modernen Projekten Anwendung findet. Die meisten aktuellen Frameworks nutzen diese Technik, um Abhängigkeiten zu lösen und somit besser wartbaren und wiederverwendbaren Code zu schreiben. Durch die Verwendung von Frameworks nutzt man also automatisch diese Technik. Doch wie funktioniert dies eigentlich "unter der Haube"? An einfachen Beispielen soll die Funktionsweise erklärt werden.

Dependency Injection - Was soll das?

Die Dependency Injection, also Abhängigkeits-Injektion, soll die Abhängigkeiten von Klassen untereinander lösen. Dies wird erreicht, indem die benötigten Abhängigkeiten (Objekte) nicht innerhalb einer Klasse erzeugt werden, sondern der Klasse von außen übergeben werden. Dadurch muss die Klasse nichts über die Instanziierung des Objektes wissen, sondern kann es einfach nutzen. Außerdem kann beim Testen der Klasse mit Mockup-Objekten gearbeitet werden.

Folgendes Beispiel übergibt die Abhängigkeiten direkt mit dem Konstruktor. Die Constructor-Injection ist die gängiste und in meinen Augen auch sauberste Möglichkeit der Dependency-Injection.

/**
 * Klassische Instanziierung des Logger-Objektes innerhalb des Konstruktors OHNE Dependency Injection
 */
class MyController extends Controller {
    private Logger $logger;
    public function __construct() {
        $this->logger = new Logger('path/to/logfile');
    }

    public function action() {
        $this->logger->log('Starting the action!');
        # do some stuff
    }
}
/**
 * Das Logger-Objekt wird im Konstruktor an die Klasse übergeben (Dependency Injection)
 */
class MyController extends Controller {
    private Logger $logger;
    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }

    public function action() {
        $this->logger->log('Starting the action!');
        # do some stuff
    }
}

Alternativ kann man auch Abhängigkeiten mittels Setter-Injection übergeben. Dabei wird eine spezielle Setter-Methode zu Übergabe der Abhängigkeiten erstellt.

/**
 * Das Logger-Objekt wird mittels Setter-Methode an die Klasse übergeben (Dependency Injection)
 */
class MyController extends Controller {
    private Logger $logger;
    public function __construct() {
    }

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function action() {
        $this->logger->log('Starting the action!');
        # do some stuff
    }
}

Die dritte Möglichkeit der Dependency-Injection ist die Method-Injection. Hierbei werden die benötigten Abhängigkeiten direkt an die jeweiligen Methoden übergeben.

class MyController extends Controller {
    // Keine Logger-Eigenschaft auf Klassenebene notwendig

    public function __construct() {
        // Kein Logger wird hier injiziert
    }

    // Die Abhängigkeit wird direkt der Methode übergeben
    public function action(Logger $logger) {
        $logger->log('Starting the action!');
        # do some stuff
    }
}

Die Abhängigkeit ist somit aber nur teilweise gelöst. Die Klasse braucht nun zwar nichts mehr über die Instanziierung des verwendeten Objektes zu wissen, jedoch kann der direkte Zugriff auf das Objekt durch Anpassungen an der Klasse des Objektes gefährdet werden und somit zu Fehlern führen.

Dependency Injection mittels Interface

Um das Problem des direkten Zugriffs auf das Objekt zu lösen bzw. den Zugriff abzusichern, können Interfaces verwendet werden. Wenn diese Schnittstellen-Definitionen auf beiden Seiten genutzt werden, sowohl in der Klasse, die ein bestimmtes Objekt nutzen soll, als auch in der Klasse des genutzten Objektes, dann wird sichergestellt, dass mindestens die im Interface definierten Methoden vorhanden sind. Das Interface ist ein Vertrag zwischen verschiedenen Klassen, der eingehalten werden muss, um die Funktionalität abzusichern.

interface LoggerInterface {
    public function log(string $message): void;
}

class FileLogger implements LoggerInterface {
    private string $filePath;

    public function __construct(string $filePath) {
        $this->filePath = $filePath;
    }

    public function log(string $message): void {
        file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
    }
}

class MyController extends Controller {
    private LoggerInterface $logger;
    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }

    public function action() {
        $this->logger->log('Starting the action!');
        # do some stuff
    }
}

$controller = new MyController(new FileLogger('log.txt'));

Das Interface stellt sicher, dass das verwendete Objekt die Methode log() implementiert. Somit kann der Controller sich darauf verlassen, dass er diese Methode nutzen kann, ohne weitere Kenntnisse über das Logger-Objekt zu besitzen. Auch kann die Logger-Klasse einfach angepasst werden, solange sie das Interface implementiert. Das stellt sicher, das der Rest der Anwendung auch nach einer Anpassung der Klasse weiterhin funktioniert.

Fazit

Durch diese lose Kopplung mittels Interfaces wird sichergestellt, dass immer die benötigten Methoden zur Verfügung stehen, ganz gleich wie die Klassen aufgebaut sind oder angepasst werden. Die Klasse, die ein bestimmtes Objekt verwendet, muss nicht das zu verwendende Objekt kennen und benötigt auch keine Informationen über dessen Instanziierung. So kann z.B. der File-Logger ganz einfach durch einen Datenbank-Logger ausgetauscht werden, solange dieser auch das entsprechende Interface implementiert. Die restliche Anwendung wird den Austausch des Logger-Objektes nicht bemerken und ganz normal funktionieren.

Einen Schritt weiter geht die eigentliche Magie der meisten großen Frameworks bei der Bereitstellung eines DI-Containers. Diese Container werden genutzt, um Abhängigkeiten zu laden. Außerdem sind sie in der Lage, automatisch die in einem Konstruktor angegebenen Abhängigkeiten zu bedienen.

PHP und Dependency Injection (DI) - Teil 2 befasst sich damit, wie die automatische Bereitstellung der Abhängigkeiten durch einen einfachen DI-Container funktionieren kann und welche Vorteile dies in der Praxis bietet.

⤎ Vorheriger Eintrag | Nächster Eintrag ⤏