Abhängigkeiten in Legacy-Systemen verwaltbar machen

Seite 3: Container einsetzen

Inhaltsverzeichnis

Container lassen sich direkt in Controllern verwenden, andere Klassen (insbesondere bei neu entwickeltem Code) sollten ihre Abhängigkeiten allerdings explizit hereingereicht bekommen. Während der Migration des Legacy-Codes kann es praktisch sein, den Container auch direkt aus einem Service zu benutzen, um die Migrationsschritte klein zu halten. Für Tests sind Mocks der benötigten Services im Container zu registrieren. Nach der Überarbeitung sollte die Abhängigkeit zum Container allerdings nicht mehr nötig sein.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\Config\ConfigCache;

class CachedContainerLoader
{
/**
* @var array
*/
private $config;

/**
* @param array $config
*/
public function __construct(array $config)
{
$this->config = $config;
}

/**
* Initializes the service container.
*
* The cached version of the service container is used ↵
when fresh, otherwise the container is built.
*
* @param bool $forceCacheRefresh
*
* @return YiiContainerComponent
*/
public function getContainer($forceCacheRefresh = false)
{
$class = 'CachedContainer';
$cache = new ConfigCache(APP_ROOT.'/runtime/'.$class.'.php',↵
YII_DEBUG);

if (!$cache->isFresh() || $forceCacheRefresh) {
/* @var $component YiiContainerComponent */
$container = new YiiContainerComponent();
$container->setConfiguration($this->config);
$container->init();

$this->dumpContainer($cache, $container, $class, ↵
'YiiContainerComponent');
}

/** @noinspection PhpIncludeInspection */
require_once $cache;
/* @var $container YiiContainerComponent */
$container = new $class();

// set synthetic services
$container->set('db', Yii::app()->db);

$container->setIsInitialized(); // prevent reinitialisation

return $container;
}

/**
* Dumps the service container to PHP code in the cache.
*
* @param ConfigCache $cache The config cache.
* @param ContainerBuilder $container The service container.
* @param string $class The name of the class to ↵
generate.
* @param string $baseClass The name of the container's ↵
base class.
*/
private function dumpContainer(ConfigCache $cache, ↵
ContainerBuilder $container, $class, $baseClass)
{
$dumper = new PhpDumper($container);

$content = $dumper->dump(['class' => $class, 'base_class' =>
$baseClass, 'file' => (string) $cache]);

$cache->write($content, $container->getResources());
}
}

Wenn das Programm den Container mit vielen Services bei jedem Request neu bauen muss, wirkt sich das merklich auf die Performance aus. Deswegen gibt es die Option, den fertig konfigurierten Controller als PHP-Klasse zu generieren und zwischenzuspeichern. Dabei ist zu berücksichtigen, dass der Cache bei Änderungen der Konfiguration, Klassennamen oder Namespaces ungültig zu machen ist. Für den Zweck enthält das vorangegangene Listing eine Loader-Klasse, die in der Bootstrap-Datei zum Einsatz kommt:

// ...

$app = Yii::createWebApplication($config);
$container = (new CachedContainerLoader([
'userRepository' => [
'class' => 'Module\User\Service\UserRepository',
'arguments' => ['@db'],
],
'userService' => [
'class' => 'Module\User\Service\UserService',
'arguments' => ['/data/users'],
],
]))->getContainer();
$app->setComponent('container', $container);
$app->run();

Mit einer wachsenden Zahl von Services ist die direkte Konfiguration dort allerdings nicht mehr praktikabel. Als Alternative kommen YAML-Dateien in Frage. Dabei bietet es sich in einem großen Projekt zudem an, den Code fachlich in Module zu gliedern. Es sollte dann entsprechend auch Servicekonfigurationen pro Modul geben. Die dort definierten Services werden automatisch im Container registriert:

config/services.yml

parameters:
baseDataDirectory: "/data"

db:
synthetic: true

modules/User/config/services.yml

services:
userReposirory:
class: Module\User\Service\UserRepository
arguments: [@db]

userService:
class: Module\User\Service\UserService
arguments: ["%baseDataDirectory%/user"]

Die Container-Komponente ist dafür so anzupassen, dass sie die YAML-Dateien parst, statt die Services direkt zu konfigurieren. Symfony bietet dafür einige Möglichkeiten, derer sich das folgende Quelltextbeispiel bedient:

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class YiiContainerComponent extends ContainerBuilder implements ↵
IApplicationComponent
{
/**
* @var bool
*/
private $isInitialized = false;

/**
* @var array
*/
private $configuration = [];

public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
}

public function init()
{
$loader = new YamlFileLoader($this, new ↵
FileLocator(APP_ROOT.'/config'));
foreach ($this->configuration['files'] as $aFile) {
$loader->load($aFile.'.yml');
}

foreach ($this->configuration['modules'] as $aModuleName) ↵
{ // TODO: Use Yii config to determine module names.
$this->getModule($aModuleName)->loadServices($this);
}

$this->isInitialized = true;
}

/**
* @return bool
*/
public function getIsInitialized()
{
return $this->isInitialized;
}

public function setIsInitialized()
{
$this->isInitialized = true;
}

/**
* @param $moduleName
*
* @return \Module\BaseModule|CModule
*/
private function getModule($moduleName)
{
return Yii::app()->getModule($moduleName);
}
}

Damit sich die Konfigurationen auf die Module verteilen lassen, ist die Yii-Module-Klasse wie folgt zu erweitern:

namespace Module;

use CWebModule;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class BaseModule extends CWebModule
{
// ...

/**
* Will load service from service.yml file located in config ↵
folder.
*
* @param ContainerBuilder $container
*/
public function loadServices(ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new ↵
FileLocator(APP_ROOT.'/modules/'.$this->getName().'/config/'));

foreach (['services'] as $aFile) { // TODO: Inject file names ↵
through Yii config.
$loader->load($aFile.'.yml');
}
}
}

Außerdem ist eine Änderung der Bootstrap-Datei nötig:

// ...

$app = Yii::createWebApplication($config);
$container = (new CachedContainerLoader([
'files' => ['services'],
'modules' => ['User'],
]))->getContainer(true);
$app->setComponent('container', $container);
$app->run();

Um das System nun weiter zu verbessern, wäre eine Option, alle existierenden Yii-Module auf Container-Konfigurationen zu prüfen, statt die Modulnamen explizit zu übergeben. Im Beispiel wurde an der Stelle bewusst auf die Yii-Konfiguration verzichtet. Statt dessen hat der Autor die benötigten Einstellungen explizit gesetzt, damit die Komplexität gering bleibt.