Reputation: 3760
For my project I want to specify some custom configuration. I have a bunch of 'mappers' that have some properties and will refer to other services.
For example, I would like my config to look like this:
self_service:
mappers:
branche_vertalingen:
data_collector: "@self_service.branche_vertalingen.data_collector"
data_loader: "@self_service.branche_vertalingen.data_loader"
map_data: SelfServiceBundle\Entity\BrancheVertalingMapData
Where self_service
is the bundle name, mappers
is the 'container' where all the mappers are defined. And branche_vertalingen
is one of the defined mappers, there can (and will) be many more. At the moment, each mapper has a data_collector
and a data_loader
that refer to services defined in the bundle's services.yml
, and a map_data
property which refers to an entity's class name.
I have put this configuration in SelfServiceBundle/Resources/config/config.yml
and import it in app/config/config.yml
.
I have create a SelfServiceExtension
class according to this article. In the extension's load()
method I receive my defined configuration as an array. So far, so good.
The problem I am having is that the value for data_collector
I receive is just the defined string, and not the service I was expecting. No problem, I thought. I have a $container
available, I will just look it up, but I can't get the service there.
The question: How do I make sure I can get the service I reference in the config?
I had already tried doing the same in a parameters
block so that I wouldn't even need a bundle Extension, but doing that I got this error: You cannot dump a container with parameters that contain references to other services
. So after that I tried to do it via an Extension.
Upvotes: 3
Views: 108
Reputation: 54659
As I wrote in the comments, I think tagged services are a good fit for this. It allows you to very easily add or remove mappers just by tagging a service. This way there's no hard requirement for all mappers to live at the same place or similar.
Using interfaces to ensure that everything is wired correctly also allows for easy extension.
To see how this can even incorporate your initial idea, see the example at the end of this answer.
/** @var $mapperManager MapperManager */
$mapperManager = $this->get('app.mapper_manager');
dump($mapperManager);
foreach ($mapperManager->getMappers('branche_vertalingen') as $mapper) {
dump($mapper);
}
(closely following the official docs):
service.yml
services:
app.mapper_manager:
class: AppBundle\Mapper\Manager
# mappers
app.mapper_1:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "a"
- "b"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: branche_vertalingen }
app.mapper_2:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "c"
- "d"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: branche_vertalingen }
app.mapper_3:
public: false
class: AppBundle\Mapper\DefaultMapper
arguments:
- "e"
- "f"
- SelfServiceBundle\Entity\BrancheVertalingMapData
tags:
- { name: app.mapper, branch: other_branch }
the compiler pass:
<?php
// src/AppBundle/DependencyInjection/Compiler/MapperCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class MapperCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('app.mapper_manager')) {
return;
}
$definition = $container->findDefinition('app.mapper_manager');
$taggedServices = $container->findTaggedServiceIds('app.mapper');
foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
$definition->addMethodCall('addMapper', [$attributes['branch'], new Reference($id)]);
}
}
}
}
using the compiler pass:
<?php
// src/AppBundle/AppBundle.php
namespace AppBundle;
use AppBundle\DependencyInjection\Compiler\MapperCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new MapperCompilerPass());
}
}
a simple manager class (call it whatever fits better):
<?php
// src/AppBundle/Mapper/Manager.php
namespace AppBundle\Mapper;
class Manager
{
private $mappers = [];
public function addMapper($branch, MapperInterface $mapper)
{
if (!array_key_exists($branch, $this->mappers)) {
$this->mappers[$branch] = [];
}
$this->mappers[$branch][] = $mapper;
}
public function getMappers($branch)
{
if (!array_key_exists($branch, $this->mappers)) {
// handle invalid access
// throw new \InvalidArgumentException('%message%');
}
return $this->mappers[$branch];
}
}
a default mapper class (this is actually not required, but could make things easier to start with):
<?php
// src/AppBundle/Mapper/DefaultMapper.php
namespace AppBundle\Mapper;
class DefaultMapper implements MapperInterface
{
private $dataCollector;
private $dataLoader;
private $mapData;
public function __construct($dataCollector, $dataLoader, $mapData)
{
$this->dataCollector = $dataCollector;
$this->dataLoader = $dataLoader;
$this->mapData = $mapData;
}
public function getDataCollector()
{
return $this->dataCollector;
}
public function getDataLoader()
{
return $this->dataLoader;
}
public function getMapData()
{
return $this->mapData;
}
}
and finaly a simple interface to use with the data mappers:
<?php
// src/AppBundle/Mapper/MapperInterface.php
namespace AppBundle\Mapper;
interface MapperInterface
{
public function getDataCollector();
public function getDataLoader();
public function getMapData();
}
With an additional compiler pass (or only, see code-comments) you could also extend the above solution:
using an extra compiler pass, like:
<?php
// src/AppBundle/DependencyInjection/Compiler/MapperCollectionCompilerPass.php
namespace AppBundle\DependencyInjection\Compiler;
use AppBundle\Mapper\DefaultMapper;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;
class MapperCollectionCompilerPass implements CompilerPassInterface
{
private $parameterName;
public function __construct($parameterName)
{
$this->parameterName = $parameterName;
}
public function process(ContainerBuilder $container)
{
if (!$container->has('app.mapper_manager')) {
return;
}
if (!$container->hasParameter($this->parameterName)) {
return;
}
$definition = $container->findDefinition('app.mapper_manager');
$mappers = $container->getParameter($this->parameterName);
foreach ($mappers as $branch => $meta) {
$mapper = new Definition(DefaultMapper::class, [
new Reference($meta['data_collector']),
new Reference($meta['data_loader']),
$meta['map_data'],
]);
$mapper
->setPublic(false)
->addTag('app.mapper', ['branch' => $branch])
;
$container->addDefinitions([$mapper]);
// If you don't want to use tags, simply add the 'addMethodCall'
// from MapperCompilerPass here
// $definition->addMethodCall('addMapper', [$branch, $mapper]);
}
}
}
adding it to the bundle:
<?php
// src/AppBundle/AppBundle.php
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use AppBundle\DependencyInjection\Compiler\MapperCompilerPass;
use AppBundle\DependencyInjection\Compiler\MapperCollectionCompilerPass;
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new MapperCollectionCompilerPass('mappers'));
$container->addCompilerPass(new MapperCompilerPass());
}
}
and adding the config:
# app/config/services.yml
parameters:
mappers:
branche_vertalingen:
# !note the missing @
data_collector: app.some_service
data_loader: app.some_service
map_data: SelfServiceBundle\Entity\BrancheVertalingMapData
Upvotes: 2