Klassen in PHP haben immer wieder Abhängigkeiten zu anderen Klassen – sogenannten Diensten (Service). Um diese möglichst flexibel und einfach verwalten zu können wird ein Service-Container benötigt. In diesem Blogbeitrag erläutere ich euch die Vorteile eines solchen Containers und gebe ein Beispiel wie er aussehen könnte.
Probleme mit Abhängigkeiten
Wozu wir einer globale Verwaltung unserer Dienste benötigen, lässt sich am besten durch ein Beispiel erklären. Die Ausgangssituation ist, dass ein Controller etwas aus dem Cache laden möchte.
1 2 3 4 5 6 7 8 9 |
class IndexController { public function indexAction() { $cache = new \Project\Library\MyXCache(); // ... } } |
Hier entsteht eine Abhängigkeit zu MyXCache
. Ein UnitTest der Funktion würde immer auch MyXCache
berühren, was zu unangenehmen Nebeneffekten führen kann, was die Funktion schwer testbar macht.
Sollte die Instanzierung der Klasse MyXCache
unperformant sein oder projektbezogene Einstellungen benötigen (z. B. Datenbankkonfiguration), müssen wir die Kosten bei jedem Aufruf erneut bezahlen. Der selbe Dienst lässt sich nur schwer über mehrere Funktionen und Klassen hinweg zu teilen.
Ein weiterer Nachteil ist die schlechte Wartbarkeit des Programmcodes. Müssen wir einen Dienst austauschen (hier z. B. eine Umstellung auf einen anderen Cache), muss jede Stelle im Code geändert werden, die diesen Dienst benutzt.
Diese Probleme können alle mit entsprechender Dependency Injection gelöst werden.
Beispiel eines Service-Containers
Jedes PHP-Framework bietet seine eigene Form zur Verwaltung der Dienste. Symfony2 zum Beispiel versteckt den meisten Teil und erlaubt eine einfache Konfiguration über XML oder ähnliches. Phalcon zeigt etwas mehr des Prozesses und gibt uns den Container selbst zur Manipulation frei. Daran orientiert sich auch mein Beispiel.
Solltet ihr es also doch einmal selbst programmieren müssen, oder einfach wissen wollen wie es intern aussehen könnte, dann ist hier der entsprechende Code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
/** * Dependency Injector. * Hier werden alle von der Anwendung global verfügbare Dienste hinterlegt. */ class DI { private static $instance; private $container = []; private function __construct() { } private function __clone() { } public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Einen Service hinterlegen. * * @param string $name Name des Service. * @param string|object|callable $classDefinition Definition wie die Instanz zu erstellen ist. Kann ein Klassenname oder eine Funktion sein. * @param bool $shared Gibt es nur eine Instanz für alle, oder bekommt jeder eine eigene. */ public function set($name, $classDefinition, $shared = false) { $this->container[$name] = (object)['def' => $classDefinition, 'shared' => $shared, 'instance' => null]; } /** * Einen Service abrufen. * * @param string $name * * @return object */ public function get($name) { if (!isset($this->container[$name])) { throw new \RuntimeException('Requested service "' . $name . '" not defined.'); } $service = $this->container[$name]; if (!$service->shared) { // Wird nicht gemeinsam verwendet, also immer eine neue Instanz erstellen. return $this->createInstance($service->def, false); } if ($service->instance === null) { // Service wurde bisher noch nicht angefordert, also neue Instanz erstellen. $service->instance = $this->createInstance($service->def, true); } return $service->instance; } /** * Erstellt eine Instanz der Klasse. * * @param string|object|callable $definition * * @param bool $shared * * @return object */ private function createInstance($definition, $shared) { if (is_callable($definition)) { // Benutzer hat eine Funktion hinterlegt, die die Klasse erstellt. return $definition(); } if (is_string($definition)) { // Einfacher Klassenname return new $definition; } if (is_object($definition)) { if ($shared) { return $definition; } else { return new $definition; } } throw new \RuntimeException('Malformed service definition!'); } } |
Verwendung
Definition der Dienste
Definiert werden unsere Dienste zu Beginn der Anwendung (Bootstrap) – im einfachsten Fall also direkt in der index.php
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$di = DI::getInstance(); // Hinterlegen einer Instanz der Klasse; Service wird gemeinsam verwendet $di->set('memcache', new \Project\Library\MyMemcache(), true); // Hinterlegen eines Klassennamens. Nützlich, wenn der Konstruktor keine Argumente benötigt und die Klasse nicht oft verwendet wird. $di->set('cache', '\Project\Library\MyXCache'); // Defintion mit einer Funktion. Nützlich, wenn die Instanzierung teuer oder komplizierter ist. // Da in der gesamten Anwendung die selbe Datenbankverbindung genutzt werden soll, setzten wir shared auf true. $di->set('db', function () { return new \PDO('mysql:dbname=testdb;host=127.0.0.1', 'root', '123'); }, true); |
Verwendung der Dienste
Mit einer derartigen Dependency Injection im Projekt verändert sich unser Beispiel von oben folgendermaßen:
1 2 3 4 5 6 7 8 9 |
class IndexController { public function indexAction() { $cache = DI::getInstance()->get('cache'); // ... } } |
UnitTests mit Depency Injection
Wie oben schon erwähnt, behindern feste Abhängigkeiten unsere Testmöglichkeiten. Jetzt allerdings können wir die notwendigen Dienste im Test als Mock-Objekte im Service-Container hinterlegen. Der zugehörige Test könnte dann zum Beispiel so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 |
class IndexControllerTest extends \PHPUnit_Framework_TestCase { public function testIndexAction() { $mock = $this->getMock('\Project\Library\MyCacheInterface'); $mock->expects($this->once())->method('get')->with('somekey')->will($this->returnValue('somevalue')); DI::getInstance()->set('cache', $mock, true); $class = new IndexController(); $class->indexAction(); } } |