Problem
Mam projekt w Symfony, który ma kilka klas komend („Command”) będących parserami (nie ma znaczenia czego w kontekście tego tekstu). W „AppBundle/Command” są więc klasy wg. schematu nazwy [Nazwa]ParseCommand.php.
Potrzebuję rejestrować te klasy jako serwisy. Do tej pory robiłem to w „services.yml”, np.:
1 2 3 4 5 6 |
app.command.abc_parse_command: class: AppBundle\Command\AbcParseCommand tags: - { name: console.command } calls: - [setContainer, ["@service_container"] |
Jednakże nie jest to zbyt wygodne – trzeba pamiętać, żeby zarejestrować serwis przy dodawaniu kolejnego parsera.
Rozwiązanie – service definition object
Rozwiązaniem jest dynamiczne rejestrowanie serwisów przy pomocy service definition object.
Potrzebujemy dodać generowanie serwisów do procesu „kompilowania”.
- W klasie „AppBundle/AppBundle.php” (w moim przypadku bundle nazywa się „AppBundle”), dodajemy nowy obiekt klasy odpowiadającej za generowanie serwisów:
12345678910111213141516<?phpnamespace AppBundle;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\HttpKernel\Bundle\Bundle;class AppBundle extends Bundle{public function build(ContainerBuilder $container){parent::build($container);$container->addCompilerPass(new CustomPass());}} - Implementujemy klasę „AppBundle/CustomerPass.php”:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748<?phpnamespace AppBundle;use Doctrine\Common\Inflector\Inflector;use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Definition;use Symfony\Component\DependencyInjection\Reference;use Symfony\Component\Finder\Finder;class CustomPass implements CompilerPassInterface{public function process(ContainerBuilder $container){$finder = new Finder();$finder->files()->in(__DIR__ . '/Command');$finder->files()->name('/^[\w]+ParseCommand\.php$/');foreach ($finder as $file) {$name = $file->getBasename('.php');$this->prepareParserServiceDefinition($container, $name);}}/*** @param ContainerBuilder $container* @param $name*/protected function prepareParserServiceDefinition(ContainerBuilder $container, string $name){$definition = new Definition('AppBundle\Command\\' . $name);$definition->addTag('console.command');$definition->addMethodCall('setContainer', [new Reference('service_container')]);$serviceName = $this->generateParserServiceName($name);$container->setDefinition($serviceName, $definition);}protected function generateParserServiceName(string $className){$inflector = \ICanBoogie\Inflector::get();return 'app.command.' . $inflector->underscore($className);}}
Klasa musi implementować interfejs „CompilerPassInterface” z metodą „process”.
W moim przykładzie w metodzie tej przy pomocy obiektu klasy „Finder” znajdujemy nazwy wszystkich klas pasujące do naszego wzorca, po czym przepuszczamy ją przez logikę tworzącą „service definition object” (metoda „prepareParserServiceDefinition”).
Tworzenie definicji to dokładnie ten kod:
123$definition = new Definition('AppBundle\Command\\' . $name);$definition->addTag('console.command');$definition->addMethodCall('setContainer', [new Reference('service_container')]);
Moje klasy wymagają wywołania metody „setContainer” – jak można zaobserwować na przedstawionym wcześniej fragmencie pliku „services.yml” – odpowiednikiem tego jest wywołanie metody „addMethodCall” na obiekcie definicji serwisu.
Podsumowanie
Przedstawiłem przykład dynamicznego rejestrowania komend jako serwisów w Symfony.