Skip to content

[DependencyInjection] ActionBundle integration: introduce _instanceof #21357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@ public function __construct(ContainerBuilder $container, FileLocatorInterface $l

parent::__construct($locator);
}

/**
* Generates the ID of an "instanceof" definition.
*
* @param string $id
* @param string $type
* @param string $file
*
* @return string
*/
protected function generateInstanceofDefinitionId($id, $type, $file)
{
return sprintf('0%s_%s_%s', $id, $type, hash('sha256', $file));
}
}
52 changes: 46 additions & 6 deletions src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,9 @@ private function parseDefinitions(\DOMDocument $xml, $file)
}

$defaults = $this->getServiceDefaults($xml, $file);
$instanceof = $xpath->query('//container:services/container:instanceof/container:service') ?: null;
foreach ($services as $service) {
if (null !== $definition = $this->parseDefinition($service, $file, $defaults)) {
if (null !== $definition = $this->parseDefinition($service, $file, $defaults, $instanceof)) {
$this->container->setDefinition((string) $service->getAttribute('id'), $definition);
}
}
Expand Down Expand Up @@ -182,14 +183,16 @@ private function getServiceDefaults(\DOMDocument $xml, $file)
/**
* Parses an individual Definition.
*
* @param \DOMElement $service
* @param string $file
* @param array $defaults
* @param \DOMElement $service
* @param string $file
* @param array $defaults
* @param \DOMNodeList|null $instanceof
*
* @return Definition|null
*/
private function parseDefinition(\DOMElement $service, $file, array $defaults = array())
private function parseDefinition(\DOMElement $service, $file, array $defaults = array(), \DOMNodeList $instanceof = null)
{
$id = (string) $service->getAttribute('id');
if ($alias = $service->getAttribute('alias')) {
$this->validateAlias($service, $file);

Expand All @@ -199,11 +202,48 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults =
} elseif (isset($defaults['public'])) {
$public = $defaults['public'];
}
$this->container->setAlias((string) $service->getAttribute('id'), new Alias($alias, $public));
$this->container->setAlias($id, new Alias($alias, $public));

return;
}

$definition = $this->getDefinition($service, $file, $defaults);
if (null === $instanceof) {
return $definition;
}

$className = $definition->getClass() ?: $id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is broken when the class is a parameter (IIRC, this is still supported in 3.x, even though Symfony itself does not use the feature anymore)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, it cannot be fixed in the loader directly because all parameters may have not been set at this time...

Copy link
Member Author

@dunglas dunglas Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have 2 options here:

  1. Don't support parameters as id when using _instanceof and document this behavior
  2. Store the something like the following and the container and create child definitions in a compiler pass
['/foo/bar.yaml' => [
  'services' => ['id1', 'id2'],
  'instanceof' => ['type1' => new Definition(/*...*/), 'type2' => new Definition(/*...*/)]
]]

For the sake of simplicity, I'm for the solution 1. Solution 2 will create new edges cases (when using inline definitions for instance).
Or maybe someone have a better idea?

$parentId = $definition instanceof ChildDefinition ? $definition->getParent() : null;
foreach ($instanceof as $s) {
$type = (string) $s->getAttribute('id');
if (!is_a($className, $type, true)) {
continue;
}

if ($parentId) {
$s = clone $s;
$s->setAttribute('parent', $parentId);
Copy link
Contributor

@GuilhemN GuilhemN Jan 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it throw an exception if there is already a parent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue here. This is altering the DOM node, which means it will impact the next services too (which will break things if they are not ChildDefinition themselves)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}
$parentId = $this->generateInstanceofDefinitionId($id, $type, $file);
$parentDefinition = $this->getDefinition($s, $file);
$parentDefinition->setAbstract(true);
if ($parentDefinition instanceof ChildDefinition) {
$definition->setInheritTags(true);
}
$this->container->setDefinition($parentId, $parentDefinition);
}

if (null !== $parentId) {
$service->setAttribute('parent', $parentId);
$definition = $this->getDefinition($service, $file, $defaults);
$definition->setInheritTags(true);
}

return $definition;
}

private function getDefinition(\DOMElement $service, $file, array $defaults = array())
{
if ($parent = $service->getAttribute('parent')) {
$definition = new ChildDefinition($parent);

Expand Down
92 changes: 72 additions & 20 deletions src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,16 @@ private function parseDefinitions(array $content, $file)
}

$defaults = $this->parseDefaults($content, $file);

if ($this->isUnderscoredParamValid($content, '_instanceof', $file)) {
$instanceof = $content['services']['_instanceof'];
unset($content['services']['_instanceof']);
} else {
$instanceof = array();
}

foreach ($content['services'] as $id => $service) {
$this->parseDefinition($id, $service, $file, $defaults);
$this->parseDefinition($id, $service, $file, $defaults, $instanceof);
}
}

Expand All @@ -174,20 +182,13 @@ private function parseDefinitions(array $content, $file)
*/
private function parseDefaults(array &$content, $file)
{
if (!isset($content['services']['_defaults'])) {
return $content;
}
if (!is_array($defaults = $content['services']['_defaults'])) {
throw new InvalidArgumentException(sprintf('Service defaults must be an array, "%s" given in "%s".', gettype($defaults), $file));
if (!$this->isUnderscoredParamValid($content, '_defaults', $file)) {
return array();
}
if (isset($defaults['alias']) || isset($defaults['class']) || isset($defaults['factory'])) {
@trigger_error('Giving a service the "_defaults" name is deprecated since Symfony 3.3 and will be forbidden in 4.0. Rename your service.', E_USER_DEPRECATED);

return $content;
}

$defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire');
$defaults = $content['services']['_defaults'];
unset($content['services']['_defaults']);
$defaultKeys = array('public', 'tags', 'inherit_tags', 'autowire');

foreach ($defaults as $key => $default) {
if (!in_array($key, $defaultKeys)) {
Expand Down Expand Up @@ -226,17 +227,37 @@ private function parseDefaults(array &$content, $file)
return $defaults;
}

private function isUnderscoredParamValid($content, $name, $file)
{
if (!isset($content['services'][$name])) {
return false;
}

if (!is_array($underscoreParam = $content['services'][$name])) {
throw new InvalidArgumentException(sprintf('Service "%s" key must be an array, "%s" given in "%s".', $name, gettype($underscoreParam), $file));
}

if (isset($underscoreParam['alias']) || isset($underscoreParam['class']) || isset($underscoreParam['factory'])) {
@trigger_error(sprintf('Giving a service the "%s" name is deprecated since Symfony 3.3 and will be forbidden in 4.0. Rename your service.', $name), E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we deprecate using _ as the first char of a service id entirely to open the door for future reserved names ?

All shortcuts we are adding in 3.3 will make it almost impossible to implement such detection in 3.4+ (class is now optional)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to deprecate all services starting by _.

Copy link
Member

@nicolas-grekas nicolas-grekas Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mixed on this deprecation at the general service id level.
But I'm 👍 for deprecating such ids when using the YamlFileLoader

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, we have an inconsistency here: _defaults and _instanceof are allowed as service ids in other loaders.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be fixed if we deprecate all services starting with a _ as you suggested.


return false;
}

return true;
}

/**
* Parses a definition.
*
* @param string $id
* @param array|string $service
* @param string $file
* @param array $defaults
* @param array $instanceof
*
* @throws InvalidArgumentException When tags are invalid
*/
private function parseDefinition($id, $service, $file, array $defaults)
private function parseDefinition($id, $service, $file, array $defaults, array $instanceof)
{
if (is_string($service) && 0 === strpos($service, '@')) {
$public = isset($defaults['public']) ? $defaults['public'] : true;
Expand All @@ -253,12 +274,6 @@ private function parseDefinition($id, $service, $file, array $defaults)
$service = array();
}

if (!is_array($service)) {
throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but %s found for service "%s" in %s. Check your YAML syntax.', gettype($service), $id, $file));
}

static::checkDefinition($id, $service, $file);

if (isset($service['alias'])) {
$public = array_key_exists('public', $service) ? (bool) $service['public'] : (isset($defaults['public']) ? $defaults['public'] : true);
$this->container->setAlias($id, new Alias($service['alias'], $public));
Expand All @@ -272,6 +287,43 @@ private function parseDefinition($id, $service, $file, array $defaults)
return;
}

$definition = $this->getDefinition($id, $service, $file, $defaults);
$className = $definition->getClass() ?: $id;
$parentId = $definition instanceof ChildDefinition ? $definition->getParent() : null;
foreach ($instanceof as $type => $value) {
if (!is_a($className, $type, true)) {
continue;
}

if ($parentId) {
$value['parent'] = $parentId;
}
$parentId = $this->generateInstanceofDefinitionId($id, $type, $file);
$parentDefinition = $this->getDefinition($parentId, $value, $file, array());
$parentDefinition->setAbstract(true);
if ($parentDefinition instanceof ChildDefinition) {
$definition->setInheritTags(true);
}
$this->container->setDefinition($parentId, $parentDefinition);
}

if (null !== $parentId) {
$service['parent'] = $parentId;
$definition = $this->getDefinition($id, $service, $file, $defaults);
$definition->setInheritTags(true);
}

$this->container->setDefinition($id, $definition);
}

private function getDefinition($id, $service, $file, array $defaults)
{
if (!is_array($service)) {
throw new InvalidArgumentException(sprintf('A service definition must be an array or a string starting with "@" but %s found for service "%s" in %s. Check your YAML syntax.', gettype($service), $id, $file));
}

static::checkDefinition($id, $service, $file);

if (isset($service['parent'])) {
$definition = new ChildDefinition($service['parent']);

Expand Down Expand Up @@ -430,7 +482,7 @@ private function parseDefinition($id, $service, $file, array $defaults)
}
}

$this->container->setDefinition($id, $definition);
return $definition;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<xsd:choice maxOccurs="unbounded">
<xsd:element name="service" type="service" minOccurs="1" />
<xsd:element name="defaults" type="defaults" minOccurs="0" maxOccurs="1" />
<xsd:element name="instanceof" type="instanceof" minOccurs="0" maxOccurs="1" />
</xsd:choice>
</xsd:complexType>

Expand Down Expand Up @@ -135,6 +136,12 @@
<xsd:attribute name="inherit-tags" type="boolean" />
</xsd:complexType>

<xsd:complexType name="instanceof">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="service" type="service" minOccurs="1" />
</xsd:choice>
</xsd:complexType>

<xsd:complexType name="tag">
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:anyAttribute namespace="##any" processContents="lax" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<instanceof>
<service id="Symfony\Component\DependencyInjection\Tests\Loader\BarInterface" lazy="true">
<autowire>__construct</autowire>
<tag name="foo" />
<tag name="bar" />
</service>
</instanceof>

<service id="Symfony\Component\DependencyInjection\Tests\Loader\Bar" class="Symfony\Component\DependencyInjection\Tests\Loader\Bar" />
</services>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
_instanceof:
Symfony\Component\DependencyInjection\Tests\Loader\FooInterface:
autowire: true
lazy: true
tags:
- { name: foo }
- { name: bar }

Symfony\Component\DependencyInjection\Tests\Loader\Foo: ~
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Loader;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand Down Expand Up @@ -658,4 +659,33 @@ public function testDefaultsWithAutowiredMethods()
$this->assertSame(array('setFoo'), $container->getDefinition('no_defaults_child')->getAutowiredMethods());
$this->assertSame(array(), $container->getDefinition('with_defaults_child')->getAutowiredMethods());
}

public function testInstanceof()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services32.xml');

$definitions = $container->getDefinitions();
$parentDefinition = array_shift($definitions);
$this->assertNotInstanceOf(ChildDefinition::class, $parentDefinition);
$this->assertTrue($parentDefinition->isAutowired());
$this->assertTrue($parentDefinition->isLazy());
$this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $parentDefinition->getTags());

$container->compile();

$definition = $container->getDefinition(Bar::class);
$this->assertTrue($definition->isAutowired());
$this->assertTrue($definition->isLazy());
$this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags());
}
}

interface BarInterface
{
}

class Bar implements BarInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Tests\Loader;

use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
Expand Down Expand Up @@ -405,6 +406,27 @@ public function testDefaults()
$this->assertFalse($container->getDefinition('no_defaults_child')->isAutowired());
}

public function testInstanceof()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services32.yml');

$definitions = $container->getDefinitions();
$parentDefinition = array_shift($definitions);
$this->assertNotInstanceOf(ChildDefinition::class, $parentDefinition);
$this->assertTrue($parentDefinition->isAutowired());
$this->assertTrue($parentDefinition->isLazy());
$this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $parentDefinition->getTags());

$container->compile();

$definition = $container->getDefinition(Foo::class);
$this->assertTrue($definition->isAutowired());
$this->assertTrue($definition->isLazy());
$this->assertEquals(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags());
}

/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo").
Expand All @@ -415,3 +437,11 @@ public function testDecoratedServicesWithWrongSyntaxThrowsException()
$loader->load('bad_decorates.yml');
}
}

interface FooInterface
{
}

class Foo implements FooInterface
{
}