Skip to content
Permalink
Browse files

feature #21708 [DI] Add and wire ServiceSubscriberInterface - aka exp…

…licit service locators (nicolas-grekas)

This PR was squashed before being merged into the 3.3-dev branch (closes #21708).

Discussion
----------

[DI] Add and wire ServiceSubscriberInterface - aka explicit service locators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | no test yet
| Fixed tickets | #20658
| License       | MIT
| Doc PR        | -

This PR implements the second and missing part of #20658: it enables objects to declare their service dependencies in a similar way than we do for EventSubscribers: via a static method. Here is the interface and its current description:
```php
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();
}
```

We could then have eg a controller-as-a-service implement this interface, and be auto or manually wired according to the return value of this method - using the "kernel.service_subscriber" tag to do so.
eg:

```yaml
services:
  App\Controller\FooController:
    arguments: [service_container]
    tags:
      - name: kernel.service_subscriber
        key: logger
        service: monolog.logger.foo_channel
```

The benefits are:
- it keeps the lazy-behavior gained by service locators / container injection
- it allows the referenced services to be made private from the pov of the main Symfony DIC - thus enables some compiler optimizations
- it makes dependencies autowirable (while keeping manual wiring possible)
- it does not add any strong coupling at the architecture level
- and most importantly and contrary to regular container injection, *it makes dependencies explicit* - each classes declaring which services it consumes.

Some might argue that:
- it requires to be explicit - thus more verbose. Yet many others think it's a good thing - ie it's worth it.
- some coupling happens at the dependency level, since you need to get the DI component to get the interface definition. This is something that the PHP-FIG could address at some point.

Commits
-------

c5e80a2 implement ServiceSubscriberInterface where applicable
9b7df39 [DI] Add and wire ServiceSubscriberInterface
  • Loading branch information...
fabpot committed Mar 22, 2017
2 parents fa36ce8 + c5e80a2 commit f99dfb02045ef50aeaa72072892ebd99e3b95e4c
@@ -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',
@@ -53,14 +53,8 @@

<service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
<tag name="kernel.event_subscriber" />
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="session" type="service" id="session" on-invalid="ignore" />
</argument>
</service>
</argument>
<tag name="container.service_subscriber" id="session" />
<argument type="service" id="container" />
</service>

<service id="session.save_listener" class="Symfony\Component\HttpKernel\EventListener\SaveSessionListener">
@@ -22,14 +22,8 @@

<service id="test.session.listener" class="Symfony\Component\HttpKernel\EventListener\TestSessionListener">
<tag name="kernel.event_subscriber" />
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="session" type="service" id="session" on-invalid="ignore" />
</argument>
</service>
</argument>
<tag name="container.service_subscriber" id="session" />
<argument type="service" id="container" />
</service>
</services>
</container>
@@ -11,7 +11,9 @@
namespace Symfony\Bundle\FrameworkBundle\Routing;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Config\ContainerParametersResource;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\Routing\Router as BaseRouter;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -25,7 +27,7 @@
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Router extends BaseRouter implements WarmableInterface
class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberInterface
{
private $container;
private $collectedParameters = array();
@@ -173,4 +175,14 @@ private function resolve($value)
return str_replace('%%', '%', $escapedValue);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedServices()
{
return array(
'routing.loader' => LoaderInterface::class,
);
}
}
@@ -12,6 +12,7 @@
namespace Symfony\Bundle\TwigBundle\CacheWarmer;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinderInterface;
@@ -25,7 +26,7 @@
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TemplateCacheCacheWarmer implements CacheWarmerInterface
class TemplateCacheCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface
{
protected $container;
protected $finder;
@@ -92,6 +93,16 @@ public function isOptional()
return true;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedServices()
{
return array(
'twig' => \Twig_Environment::class,
);
}
/**
* Find templates in the given directory.
*
@@ -28,7 +28,8 @@

<service id="twig.cache_warmer" class="Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer" public="false">
<tag name="kernel.cache_warmer" />
<argument type="service" id="service_container" />
<tag name="container.service_subscriber" id="twig" />
<argument type="service" id="container" />
<argument type="service" id="templating.finder" on-invalid="ignore" />
<argument type="collection" /> <!-- Twig paths -->
</service>
@@ -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
@@ -55,6 +55,7 @@ public function __construct()
new ResolveFactoryClassPass(),
new FactoryReturnTypePass($resolveClassPass),
new CheckDefinitionValidityPass(),
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(),
new AutowirePass(),
new ResolveReferencesToAliasesPass(),
@@ -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;
}
}
}
@@ -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();
}

0 comments on commit f99dfb0

Please sign in to comment.
You can’t perform that action at this time.