[DI] Add and wire ServiceSubscriberInterface

This commit is contained in:
Nicolas Grekas 2017-02-21 23:24:03 +01:00
parent fa36ce8858
commit 9b7df39865
9 changed files with 449 additions and 0 deletions

View File

@ -24,6 +24,7 @@ class UnusedTagsPass implements CompilerPassInterface
private $whitelist = array(
'console.command',
'container.service_locator',
'container.service_subscriber',
'config_cache.resource_checker',
'data_collector',
'form.type',

View File

@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----
* added "ServiceSubscriberInterface" - to allow for per-class explicit service-locator definitions
* added "container.service_locator" tag for defining service-locator services
* added anonymous services support in YAML configuration files using the `!service` tag.
* added "TypedReference" and "ServiceClosureArgument" for creating service-locator services

View File

@ -55,6 +55,7 @@ class PassConfig
new ResolveFactoryClassPass(),
new FactoryReturnTypePass($resolveClassPass),
new CheckDefinitionValidityPass(),
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(),
new AutowirePass(),
new ResolveReferencesToAliasesPass(),

View File

@ -0,0 +1,112 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;
/**
* Compiler pass to register tagged services that require a service locator.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class RegisterServiceSubscribersPass extends AbstractRecursivePass
{
private $serviceLocator;
protected function processValue($value, $isRoot = false)
{
if ($value instanceof Reference && $this->serviceLocator && 'container' === (string) $value) {
return new Reference($this->serviceLocator);
}
if (!$value instanceof Definition || $value->isAbstract() || $value->isSynthetic() || !$value->hasTag('container.service_subscriber')) {
return parent::processValue($value, $isRoot);
}
$serviceMap = array();
foreach ($value->getTag('container.service_subscriber') as $attributes) {
if (!$attributes) {
continue;
}
ksort($attributes);
if (array() !== array_diff(array_keys($attributes), array('id', 'key'))) {
throw new InvalidArgumentException(sprintf('The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "%s" given for service "%s".', implode('", "', array_keys($attributes)), $this->currentId));
}
if (!array_key_exists('id', $attributes)) {
throw new InvalidArgumentException(sprintf('Missing "id" attribute on "container.service_subscriber" tag with key="%s" for service "%s".', $attributes['key'], $this->currentId));
}
if (!array_key_exists('key', $attributes)) {
$attributes['key'] = $attributes['id'];
}
if (isset($serviceMap[$attributes['key']])) {
continue;
}
$serviceMap[$attributes['key']] = new Reference($attributes['id']);
}
$class = $value->getClass();
if (!is_subclass_of($class, ServiceSubscriberInterface::class)) {
if (!class_exists($class, false)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $this->currentId));
}
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class));
}
$this->container->addObjectResource($class);
$subscriberMap = array();
foreach ($class::getSubscribedServices() as $key => $type) {
if (!is_string($type) || !preg_match('/^\??[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $type)) {
throw new InvalidArgumentException(sprintf('%s::getSubscribedServices() must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, is_string($type) ? $type : gettype($type)));
}
if ($optionalBehavior = '?' === $type[0]) {
$type = substr($type, 1);
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
}
if (is_int($key)) {
$key = $type;
}
if (!isset($serviceMap[$key])) {
$serviceMap[$key] = new Reference($type);
}
$subscriberMap[$key] = new ServiceClosureArgument(new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE));
unset($serviceMap[$key]);
}
if ($serviceMap = array_keys($serviceMap)) {
$this->container->log($this, sprintf('Service keys "%s" do not exist in the map returned by %s::getSubscribedServices() for service "%s".', implode('", "', $serviceMap), $class, $this->currentId));
}
$serviceLocator = $this->serviceLocator;
$this->serviceLocator = 'container.'.$this->currentId.'.'.md5(serialize($value));
$this->container->register($this->serviceLocator, ServiceLocator::class)
->addArgument($subscriberMap)
->setPublic(false)
->setAutowired($value->isAutowired())
->addTag('container.service_locator');
try {
return parent::processValue($value);
} finally {
$this->serviceLocator = $serviceLocator;
}
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection;
/**
* A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method.
*
* The getSubscribedServices method returns an array of service types required by such instances,
* optionally keyed by the service names used internally. Service types that start with an interrogation
* mark "?" are optional, while the other ones are mandatory service dependencies.
*
* The injected service locators SHOULD NOT allow access to any other services not specified by the method.
*
* It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally.
* This interface does not dictate any injection method for these service locators, although constructor
* injection is recommended.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ServiceSubscriberInterface
{
/**
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
*
* For mandatory dependencies:
*
* * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name
* internally to fetch a service which must implement Psr\Log\LoggerInterface.
* * array('Psr\Log\LoggerInterface') is a shortcut for
* * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface')
*
* otherwise:
*
* * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency
* * array('?Psr\Log\LoggerInterface') is a shortcut for
* * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface')
*
* @return array The required service types, optionally keyed by service names
*/
public static function getSubscribedServices();
}

View File

@ -0,0 +1,118 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;
require_once __DIR__.'/../Fixtures/includes/classes.php';
class RegisterServiceSubscribersPassTest extends TestCase
{
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Service "foo" must implement interface "Symfony\Component\DependencyInjection\ServiceSubscriberInterface".
*/
public function testInvalidClass()
{
$container = new ContainerBuilder();
$container->register('foo', 'stdClass')
->addTag('container.service_subscriber')
;
$pass = new RegisterServiceSubscribersPass();
$pass->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "bar" given for service "foo".
*/
public function testInvalidAttributes()
{
$container = new ContainerBuilder();
$container->register('foo', 'TestServiceSubscriber')
->addTag('container.service_subscriber', array('bar' => '123'))
;
$pass = new RegisterServiceSubscribersPass();
$pass->process($container);
}
public function testNoAttributes()
{
$container = new ContainerBuilder();
$container->register('foo', 'TestServiceSubscriber')
->addArgument(new Reference('container'))
->addTag('container.service_subscriber')
;
$pass = new RegisterServiceSubscribersPass();
$pass->process($container);
$foo = $container->getDefinition('foo');
$locator = $container->getDefinition((string) $foo->getArgument(0));
$this->assertFalse($locator->isAutowired());
$this->assertFalse($locator->isPublic());
$this->assertSame(ServiceLocator::class, $locator->getClass());
$expected = array(
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass')),
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
);
$this->assertEquals($expected, $locator->getArgument(0));
}
public function testWithAttributes()
{
$container = new ContainerBuilder();
$container->register('foo', 'TestServiceSubscriber')
->setAutowired(true)
->addArgument(new Reference('container'))
->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'bar'))
->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'baz')) // should be ignored: the first wins
;
$pass = new RegisterServiceSubscribersPass();
$pass->process($container);
$foo = $container->getDefinition('foo');
$locator = $container->getDefinition((string) $foo->getArgument(0));
$this->assertTrue($locator->isAutowired());
$this->assertFalse($locator->isPublic());
$this->assertSame(ServiceLocator::class, $locator->getClass());
$expected = array(
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference('bar', 'stdClass')),
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
);
$this->assertEquals($expected, $locator->getArgument(0));
}
}

View File

@ -658,4 +658,23 @@ class PhpDumperTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_locator.php', $dumper->dump());
}
public function testServiceSubscriber()
{
$container = new ContainerBuilder();
$container->register('foo_service', 'TestServiceSubscriber')
->setAutowired(true)
->addArgument(new Reference('container'))
->addTag('container.service_subscriber', array(
'key' => 'test',
'id' => 'TestServiceSubscriber',
))
;
$container->register('TestServiceSubscriber', 'TestServiceSubscriber');
$container->compile();
$dumper = new PhpDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_subscriber.php', $dumper->dump());
}
}

View File

@ -2,6 +2,7 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
function sc_configure($instance)
{
@ -107,3 +108,20 @@ class LazyContext
$this->lazyValues = $lazyValues;
}
}
class TestServiceSubscriber implements ServiceSubscriberInterface
{
public function __construct($container)
{
}
public static function getSubscribedServices()
{
return array(
__CLASS__,
'?stdClass',
'bar' => 'stdClass',
'baz' => '?stdClass',
);
}
}

View File

@ -0,0 +1,129 @@
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* ProjectServiceContainer.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* @final since Symfony 3.3
*/
class ProjectServiceContainer extends Container
{
private $parameters;
private $targetDirs = array();
/**
* Constructor.
*/
public function __construct()
{
$this->services = array();
$this->normalizedIds = array(
'autowired.stdclass' => 'autowired.stdClass',
'psr\\container\\containerinterface' => 'Psr\\Container\\ContainerInterface',
'stdclass' => 'stdClass',
'symfony\\component\\dependencyinjection\\containerinterface' => 'Symfony\\Component\\DependencyInjection\\ContainerInterface',
'testservicesubscriber' => 'TestServiceSubscriber',
);
$this->methodMap = array(
'TestServiceSubscriber' => 'getTestServiceSubscriberService',
'autowired.stdClass' => 'getAutowired_StdClassService',
'foo_service' => 'getFooServiceService',
);
$this->privates = array(
'autowired.stdClass' => true,
);
$this->aliases = array();
}
/**
* {@inheritdoc}
*/
public function compile()
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}
/**
* {@inheritdoc}
*/
public function isCompiled()
{
return true;
}
/**
* {@inheritdoc}
*/
public function isFrozen()
{
@trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED);
return true;
}
/**
* Gets the 'TestServiceSubscriber' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return \TestServiceSubscriber A TestServiceSubscriber instance
*/
protected function getTestServiceSubscriberService()
{
return $this->services['TestServiceSubscriber'] = new \TestServiceSubscriber();
}
/**
* Gets the 'foo_service' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* This service is autowired.
*
* @return \TestServiceSubscriber A TestServiceSubscriber instance
*/
protected function getFooServiceService()
{
return $this->services['foo_service'] = new \TestServiceSubscriber(new \Symfony\Component\DependencyInjection\ServiceLocator(array('TestServiceSubscriber' => function () {
$f = function (\TestServiceSubscriber $v) { return $v; }; return $f(${($_ = isset($this->services['TestServiceSubscriber']) ? $this->services['TestServiceSubscriber'] : $this->get('TestServiceSubscriber')) && false ?: '_'});
}, 'stdClass' => function () {
$f = function (\stdClass $v = null) { return $v; }; return $f(${($_ = isset($this->services['autowired.stdClass']) ? $this->services['autowired.stdClass'] : $this->getAutowired_StdClassService()) && false ?: '_'});
}, 'bar' => function () {
$f = function (\stdClass $v) { return $v; }; return $f(${($_ = isset($this->services['autowired.stdClass']) ? $this->services['autowired.stdClass'] : $this->getAutowired_StdClassService()) && false ?: '_'});
}, 'baz' => function () {
$f = function (\stdClass $v = null) { return $v; }; return $f(${($_ = isset($this->services['autowired.stdClass']) ? $this->services['autowired.stdClass'] : $this->getAutowired_StdClassService()) && false ?: '_'});
})));
}
/**
* Gets the 'autowired.stdClass' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* This service is private.
* If you want to be able to request this service from the container directly,
* make it public, otherwise you might end up with broken code.
*
* This service is autowired.
*
* @return \stdClass A stdClass instance
*/
protected function getAutowired_StdClassService()
{
return $this->services['autowired.stdClass'] = new \stdClass();
}
}