Skip to content

Commit

Permalink
feature #21530 [DependencyInjection] Add "instanceof" section for loc…
Browse files Browse the repository at this point in the history
…al interface-defined configs (nicolas-grekas, dunglas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DependencyInjection] Add "instanceof" section for local interface-defined configs

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

This is a direction follow up of #21357 on which we're working together with @dunglas. From the description posted there:

There is some work being done to include features of [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) in the core of Symfony. The goal of all those PRs is to improve the developper experience of the framework, allow to develop faster while preserving all benefits of using Symfony (strictness, modularity, extensibility...) and make it easier to learn for newcomers.

This PR implements the tagging feature of ActionBundle in a more generic way. It will help to get rid of `AppBundle` in the the standard edition and to register automatically some classes including commands.

Here is an example of config (that can be embedded in the standard edition) to enable those features:

```yaml
# config/services.yml
services:
    _defaults:
        autowire: ['get*', 'set*'] # Enable constructor, getter and setter autowiring for all services defined in this file

    _instanceof:
        Symfony\Component\Console\Command: # Add the console.command tag to all services defined in this file having this type
            tags: ['console.command']
            # Set tags but also other settings like "public", "autowire" or "shared" here

        Twig_ExtensionInterface:
            tags: ['twig.extension']

        Symfony\Component\EventDispatcher\EventSubscriberInterface:
            tags: ['kernel.event_subscriber']

    App\: # Register all classes in the src/Controller directory as services
        psr4: ../src/{Controller,Command,Twig,EventSubscriber}
```

It's part of our 0 config initiative: controllers and commands will be automatically registered as services and "autowired", allowing the user to create and inject new services without having to write a single line of YAML or XML.
When refactoring changes are also automatically updated and don't require to update config files. It's a big win for rapid application development and prototyping.

Of course, this is fully compatible with the actual way of defining services and it's possible to switch (or mix) approaches very easily. It's even possible to start prototyping using 0config features then switch to explicit services definitions when the project becomes mature.

Commits
-------

773eca7 [DependencyInjection] Tests + refacto for "instanceof" definitions
2fb6019 [DependencyInjection] Add "instanceof" section for local interface-defined configs
  • Loading branch information
fabpot committed Feb 17, 2017
2 parents 7259d4e + 773eca7 commit d47571f
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 49 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* [EXPERIMENTAL] added "instanceof" section for local interface-defined configs
* [EXPERIMENTAL] added "service-locator" argument for lazy loading a set of identified values and services
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
Expand Down
20 changes: 20 additions & 0 deletions src/Symfony/Component/DependencyInjection/ChildDefinition.php
Expand Up @@ -119,6 +119,16 @@ public function setFile($file)
return parent::setFile($file);
}

/**
* {@inheritdoc}
*/
public function setShared($boolean)
{
$this->changes['shared'] = true;

return parent::setShared($boolean);
}

/**
* {@inheritdoc}
*/
Expand All @@ -139,6 +149,16 @@ public function setLazy($boolean)
return parent::setLazy($boolean);
}

/**
* {@inheritdoc}
*/
public function setAbstract($boolean)
{
$this->changes['abstract'] = true;

return parent::setAbstract($boolean);
}

/**
* {@inheritdoc}
*/
Expand Down
Expand Up @@ -42,6 +42,7 @@ public function __construct()
$this->beforeOptimizationPasses = array(
100 => array(
$resolveClassPass = new ResolveClassPass(),
new ResolveDefinitionInheritancePass(),
),
);

Expand Down
@@ -0,0 +1,106 @@
<?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\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;

/**
* Applies tags and instanceof inheritance to definitions.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ResolveDefinitionInheritancePass extends AbstractRecursivePass
{
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
if ($value instanceof ChildDefinition) {
$this->resolveDefinition($value);
}
$class = $value->getClass();
if (!$class || false !== strpos($class, '%') || !$instanceof = $value->getInstanceofConditionals()) {
return parent::processValue($value, $isRoot);
}

foreach ($instanceof as $interface => $definition) {
if ($interface !== $class && (!$this->container->getReflectionClass($interface) || !$this->container->getReflectionClass($class))) {
continue;
}
if ($interface === $class || is_subclass_of($class, $interface)) {
$this->mergeDefinition($value, $definition);
}
}

return parent::processValue($value, $isRoot);
}

/**
* Populates the class and tags from parent definitions.
*/
private function resolveDefinition(ChildDefinition $definition)
{
if (!$this->container->has($parent = $definition->getParent())) {
return;
}

$parentDef = $this->container->findDefinition($parent);
if ($parentDef instanceof ChildDefinition) {
$this->resolveDefinition($parentDef);
}

if (!isset($definition->getChanges()['class'])) {
$definition->setClass($parentDef->getClass());
}

// append parent tags when inheriting is enabled
if ($definition->getInheritTags()) {
foreach ($parentDef->getTags() as $k => $v) {
foreach ($v as $v) {
$definition->addTag($k, $v);
}
}
}

$definition->setInheritTags(false);
}

private function mergeDefinition(Definition $def, ChildDefinition $definition)
{
$changes = $definition->getChanges();
if (isset($changes['shared'])) {
$def->setShared($definition->isShared());
}
if (isset($changes['abstract'])) {
$def->setAbstract($definition->isAbstract());
}
if (isset($changes['autowired_calls'])) {
$autowiredCalls = $def->getAutowiredCalls();
}

ResolveDefinitionTemplatesPass::mergeDefinition($def, $definition);

// merge autowired calls
if (isset($changes['autowired_calls'])) {
$def->setAutowiredCalls(array_merge($autowiredCalls, $def->getAutowiredCalls()));
}

// merge tags
foreach ($definition->getTags() as $k => $v) {
foreach ($v as $v) {
$def->addTag($k, $v);
}
}
}
}
Expand Up @@ -103,6 +103,26 @@ private function doResolveDefinition(ChildDefinition $definition)
$def->setLazy($parentDef->isLazy());
$def->setAutowiredCalls($parentDef->getAutowiredCalls());

self::mergeDefinition($def, $definition);

// merge autowiring types
foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
$def->addAutowiringType($autowiringType);
}

// these attributes are always taken from the child
$def->setAbstract($definition->isAbstract());
$def->setShared($definition->isShared());
$def->setTags($definition->getTags());

return $def;
}

/**
* @internal
*/
public static function mergeDefinition(Definition $def, ChildDefinition $definition)
{
// overwrite with values specified in the decorator
$changes = $definition->getChanges();
if (isset($changes['class'])) {
Expand Down Expand Up @@ -168,26 +188,5 @@ private function doResolveDefinition(ChildDefinition $definition)
foreach ($definition->getOverriddenGetters() as $k => $v) {
$def->setOverriddenGetter($k, $v);
}

// merge autowiring types
foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
$def->addAutowiringType($autowiringType);
}

// these attributes are always taken from the child
$def->setAbstract($definition->isAbstract());
$def->setShared($definition->isShared());
$def->setTags($definition->getTags());

// append parent tags when inheriting is enabled
if ($definition->getInheritTags()) {
foreach ($parentDef->getTags() as $k => $v) {
foreach ($v as $v) {
$def->addTag($k, $v);
}
}
}

return $def;
}
}
31 changes: 28 additions & 3 deletions src/Symfony/Component/DependencyInjection/Definition.php
Expand Up @@ -30,6 +30,7 @@ class Definition
private $properties = array();
private $calls = array();
private $getters = array();
private $instanceof = array();
private $configurator;
private $tags = array();
private $public = true;
Expand Down Expand Up @@ -363,6 +364,32 @@ public function getOverriddenGetters()
return $this->getters;
}

/**
* Sets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
*
* @param $instanceof ChildDefinition[]
*
* @experimental in version 3.3
*/
public function setInstanceofConditionals(array $instanceof)
{
$this->instanceof = $instanceof;

return $this;
}

/**
* Gets the definition templates to conditionally apply on the current definition, keyed by parent interface/class.
*
* @return ChildDefinition[]
*
* @experimental in version 3.3
*/
public function getInstanceofConditionals()
{
return $this->instanceof;
}

/**
* Sets tags for this definition.
*
Expand Down Expand Up @@ -736,9 +763,7 @@ public function getAutowiredCalls()
*/
public function setAutowired($autowired)
{
$this->autowiredCalls = $autowired ? array('__construct') : array();

return $this;
return $this->setAutowiredCalls($autowired ? array('__construct') : array());
}

/**
Expand Down
20 changes: 19 additions & 1 deletion src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
Expand All @@ -29,6 +30,8 @@
abstract class FileLoader extends BaseFileLoader
{
protected $container;
protected $isLoadingInstanceof = false;
protected $instanceof = array();

/**
* @param ContainerBuilder $container A ContainerBuilder instance
Expand Down Expand Up @@ -80,7 +83,22 @@ public function registerClasses(Definition $prototype, $namespace, $resource)
$prototype = serialize($prototype);

foreach ($classes as $class) {
$this->container->setDefinition($class, unserialize($prototype));
$this->setDefinition($class, unserialize($prototype));
}
}

/**
* @experimental in version 3.3
*/
protected function setDefinition($id, Definition $definition)
{
if ($this->isLoadingInstanceof) {
if (!$definition instanceof ChildDefinition) {
throw new InvalidArgumentException(sprintf('Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, get_class($definition)));
}
$this->instanceof[$id] = $definition;
} else {
$this->container->setDefinition($id, $definition->setInstanceofConditionals($this->instanceof));
}
}

Expand Down
24 changes: 19 additions & 5 deletions src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
Expand Up @@ -57,7 +57,11 @@ public function load($resource, $type = null)
$this->loadFromExtensions($xml);

// services
$this->parseDefinitions($xml, $path);
try {
$this->parseDefinitions($xml, $path);
} finally {
$this->instanceof = array();
}
}

/**
Expand Down Expand Up @@ -126,13 +130,21 @@ private function parseDefinitions(\DOMDocument $xml, $file)
}
$this->setCurrentDir(dirname($file));

$this->instanceof = array();
$this->isLoadingInstanceof = true;
$instanceof = $xpath->query('//container:services/container:instanceof');
foreach ($instanceof as $service) {
$this->setDefinition((string) $service->getAttribute('id'), $this->parseDefinition($service, $file, array()));
}

$this->isLoadingInstanceof = false;
$defaults = $this->getServiceDefaults($xml, $file);
foreach ($services as $service) {
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
if ('prototype' === $service->tagName) {
$this->registerClasses($definition, (string) $service->getAttribute('namespace'), (string) $service->getAttribute('resource'));
} else {
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
$this->setDefinition((string) $service->getAttribute('id'), $definition);
}
}
}
Expand Down Expand Up @@ -209,7 +221,9 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
return;
}

if ($parent = $service->getAttribute('parent')) {
if ($this->isLoadingInstanceof) {
$definition = new ChildDefinition('');
} elseif ($parent = $service->getAttribute('parent')) {
$definition = new ChildDefinition($parent);

if ($value = $service->getAttribute('inherit-tags')) {
Expand Down Expand Up @@ -247,7 +261,7 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
$definition->setDeprecated(true, $deprecated[0]->nodeValue ?: null);
}

$definition->setArguments($this->getArgumentsAsPhp($service, 'argument', false, (bool) $parent));
$definition->setArguments($this->getArgumentsAsPhp($service, 'argument', false, $definition instanceof ChildDefinition));
$definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
$definition->setOverriddenGetters($this->getArgumentsAsPhp($service, 'getter'));

Expand Down Expand Up @@ -422,7 +436,7 @@ private function processAnonymousServices(\DOMDocument $xml, $file)
uksort($definitions, 'strnatcmp');
foreach (array_reverse($definitions) as $id => list($domElement, $file, $wild)) {
if (null !== $definition = $this->parseDefinition($domElement, $file)) {
$this->container->setDefinition($id, $definition);
$this->setDefinition($id, $definition);
}

if (true === $wild) {
Expand Down

0 comments on commit d47571f

Please sign in to comment.