[DI] added possibility to define services with abstract arguments

This commit is contained in:
Islam93 2019-12-21 14:41:33 +03:00 committed by Nicolas Grekas
parent f4ff77cc08
commit 62fefaa59f
19 changed files with 229 additions and 3 deletions

View File

@ -0,0 +1,44 @@
<?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\Argument;
/**
* Represents an abstract service argument, which have to be set by a compiler pass or a DI extension.
*/
final class AbstractArgument
{
private $serviceId;
private $argKey;
private $text;
public function __construct(string $serviceId, string $argKey, string $text = '')
{
$this->serviceId = $serviceId;
$this->argKey = $argKey;
$this->text = $text;
}
public function getServiceId(): string
{
return $this->serviceId;
}
public function getArgumentKey(): string
{
return $this->argKey;
}
public function getText(): string
{
return $this->text;
}
}

View File

@ -6,6 +6,7 @@ CHANGELOG
* added support to autowire public typed properties in php 7.4
* added support for defining method calls, a configurator, and property setters in `InlineServiceConfigurator`
* added possibility to define abstract service arguments
5.0.0
-----

View File

@ -20,6 +20,7 @@ use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\Config\Resource\ReflectionClassResource;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@ -1215,6 +1216,8 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$value = $this->getParameter((string) $value);
} elseif ($value instanceof Expression) {
$value = $this->getExpressionLanguage()->evaluate($value, ['container' => $this]);
} elseif ($value instanceof AbstractArgument) {
throw new RuntimeException(sprintf('Argument "%s" of service "%s" is abstract%s, did you forget to define it?', $value->getArgumentKey(), $value->getServiceId(), $value->getText() ? ' ('.$value->getText().')' : ''));
}
return $value;

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Dumper;
use Composer\Autoload\ClassLoader;
use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@ -1739,6 +1740,8 @@ EOF;
return $code;
}
} elseif ($value instanceof AbstractArgument) {
throw new RuntimeException(sprintf('Argument "%s" of service "%s" is abstract%s, did you forget to define it?', $value->getArgumentKey(), $value->getServiceId(), $value->getText() ? ' ('.$value->getText().')' : ''));
} elseif (\is_object($value) || \is_resource($value)) {
throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.');
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@ -312,6 +313,14 @@ class XmlDumper extends Dumper
$element->setAttribute('type', 'binary');
$text = $this->document->createTextNode(self::phpToXml(base64_encode($value)));
$element->appendChild($text);
} elseif ($value instanceof AbstractArgument) {
$argKey = $value->getArgumentKey();
if (!is_numeric($argKey)) {
$element->setAttribute('key', $argKey);
}
$element->setAttribute('type', 'abstract');
$text = $this->document->createTextNode(self::phpToXml($value->getText()));
$element->appendChild($text);
} else {
if (\in_array($value, ['null', 'true', 'false'], true)) {
$element->setAttribute('type', 'string');

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@ -284,6 +285,8 @@ class YamlDumper extends Dumper
return $this->getExpressionCall((string) $value);
} elseif ($value instanceof Definition) {
return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']);
} elseif ($value instanceof AbstractArgument) {
return new TaggedValue('abstract', $value->getText());
} elseif (\is_object($value) || \is_resource($value)) {
throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.');
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@ -537,6 +538,10 @@ class XmlFileLoader extends FileLoader
}
$arguments[$key] = $value;
break;
case 'abstract':
$serviceId = $node->getAttribute('id');
$arguments[$key] = new AbstractArgument($serviceId, (string) $key, $arg->nodeValue);
break;
case 'string':
$arguments[$key] = $arg->nodeValue;
break;

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
@ -446,7 +447,7 @@ class YamlFileLoader extends FileLoader
}
if (isset($service['arguments'])) {
$definition->setArguments($this->resolveServices($service['arguments'], $file));
$definition->setArguments($this->resolveServices($service['arguments'], $file, false, $id));
}
if (isset($service['properties'])) {
@ -722,7 +723,7 @@ class YamlFileLoader extends FileLoader
*
* @return array|string|Reference|ArgumentInterface
*/
private function resolveServices($value, string $file, bool $isParameter = false)
private function resolveServices($value, string $file, bool $isParameter = false, string $serviceId = '', string $argKey = '')
{
if ($value instanceof TaggedValue) {
$argument = $value->getValue();
@ -795,13 +796,16 @@ class YamlFileLoader extends FileLoader
return new Reference($id);
}
if ('abstract' === $value->getTag()) {
return new AbstractArgument($serviceId, $argKey, $value->getValue());
}
throw new InvalidArgumentException(sprintf('Unsupported tag "!%s".', $value->getTag()));
}
if (\is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->resolveServices($v, $file, $isParameter);
$value[$k] = $this->resolveServices($v, $file, $isParameter, $serviceId, $k);
}
} elseif (\is_string($value) && 0 === strpos($value, '@=')) {
if (!class_exists(Expression::class)) {

View File

@ -260,6 +260,7 @@
<xsd:simpleType name="argument_type">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="abstract" />
<xsd:enumeration value="collection" />
<xsd:enumeration value="service" />
<xsd:enumeration value="expression" />

View File

@ -0,0 +1,26 @@
<?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\Argument;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
class AbstractArgumentTest extends TestCase
{
public function testAbstractArgumentGetters()
{
$argument = new AbstractArgument('foo', '$bar', 'should be defined by Pass');
$this->assertSame('foo', $argument->getServiceId());
$this->assertSame('$bar', $argument->getArgumentKey());
$this->assertSame('should be defined by Pass', $argument->getText());
}
}

View File

@ -22,6 +22,7 @@ use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@ -41,6 +42,7 @@ use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\ScalarFactory;
use Symfony\Component\DependencyInjection\Tests\Fixtures\SimilarArgumentsDummy;
use Symfony\Component\DependencyInjection\TypedReference;
@ -542,6 +544,18 @@ class ContainerBuilderTest extends TestCase
$this->assertEquals('foobar', $builder->get('foo')->arguments['foo']);
}
public function testCreateServiceWithAbstractArgument()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Argument "$baz" of service "foo" is abstract (should be defined by Pass), did you forget to define it?');
$builder = new ContainerBuilder();
$builder->register('foo', FooWithAbstractArgument::class)
->addArgument(new AbstractArgument('foo', '$baz', 'should be defined by Pass'));
$builder->get('foo');
}
public function testResolveServices()
{
$builder = new ContainerBuilder();

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@ -33,6 +34,7 @@ use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Compiler\Foo;
use Symfony\Component\DependencyInjection\Tests\Compiler\Wither;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\ScalarFactory;
use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber;
@ -1363,6 +1365,24 @@ class PhpDumperTest extends TestCase
$this->assertInstanceOf(\stdClass::class, $container->get('deprecated1'));
$this->assertInstanceOf(\stdClass::class, $container->get('deprecated2'));
}
public function testDumpServiceWithAbstractArgument()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Argument "$baz" of service "Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument" is abstract (should be defined by Pass), did you forget to define it?');
$container = new ContainerBuilder();
$container->register(FooWithAbstractArgument::class, FooWithAbstractArgument::class)
->setArgument('$baz', new AbstractArgument(FooWithAbstractArgument::class, '$baz', 'should be defined by Pass'))
->setArgument('$bar', 'test')
->setPublic(true);
$container->compile();
$dumper = new PhpDumper($container);
$dumper->dump();
}
}
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -20,6 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\XmlDumper;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument;
class XmlDumperTest extends TestCase
{
@ -237,4 +239,15 @@ class XmlDumperTest extends TestCase
$this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services_abstract.xml'), $dumper->dump());
}
public function testDumpServiceWithAbstractArgument()
{
$container = new ContainerBuilder();
$container->register(FooWithAbstractArgument::class, FooWithAbstractArgument::class)
->setArgument('$baz', new AbstractArgument(FooWithAbstractArgument::class, '$baz', 'should be defined by Pass'))
->setArgument('$bar', 'test');
$dumper = new XmlDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/xml/services_with_abstract_argument.xml', $dumper->dump());
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -21,6 +22,7 @@ use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Dumper\YamlDumper;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
@ -117,6 +119,17 @@ class YamlDumperTest extends TestCase
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_tagged_argument.yml', $dumper->dump());
}
public function testDumpServiceWithAbstractArgument()
{
$container = new ContainerBuilder();
$container->register(FooWithAbstractArgument::class, FooWithAbstractArgument::class)
->setArgument('$baz', new AbstractArgument(FooWithAbstractArgument::class, '$baz', 'should be defined by Pass'))
->setArgument('$bar', 'test');
$dumper = new YamlDumper($container);
$this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_abstract_argument.yml', $dumper->dump());
}
private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '')
{
$parser = new Parser();

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
class FooWithAbstractArgument
{
/** @var string */
private $baz;
/** @var string */
private $bar;
public function __construct(string $baz, string $bar)
{
$this->baz = $baz;
$this->bar = $bar;
}
}

View File

@ -0,0 +1,9 @@
<?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument">
<argument key="$baz" type="abstract">should be defined by FooCompilerPass</argument>
<argument key="$bar">test</argument>
</service>
</services>
</container>

View File

@ -0,0 +1,12 @@
<?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="service_container" class="Symfony\Component\DependencyInjection\ContainerInterface" public="true" synthetic="true"/>
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument" class="Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument">
<argument key="$baz" type="abstract">should be defined by Pass</argument>
<argument key="$bar">test</argument>
</service>
<service id="Psr\Container\ContainerInterface" alias="service_container" public="false"/>
<service id="Symfony\Component\DependencyInjection\ContainerInterface" alias="service_container" public="false"/>
</services>
</container>

View File

@ -0,0 +1,15 @@
services:
service_container:
class: Symfony\Component\DependencyInjection\ContainerInterface
public: true
synthetic: true
Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument
arguments: { $baz: !abstract 'should be defined by Pass', $bar: test }
Psr\Container\ContainerInterface:
alias: service_container
public: false
Symfony\Component\DependencyInjection\ContainerInterface:
alias: service_container
public: false

View File

@ -16,6 +16,7 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
@ -31,6 +32,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\ExpressionLanguage\Expression;
@ -992,4 +994,15 @@ class XmlFileLoaderTest extends TestCase
$this->assertSame(Prototype\SinglyImplementedInterface\Adapter\Adapter::class, (string) $alias);
}
public function testLoadServiceWithAbstractArgument()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('service_with_abstract_argument.xml');
$this->assertTrue($container->hasDefinition(FooWithAbstractArgument::class));
$arguments = $container->getDefinition(FooWithAbstractArgument::class)->getArguments();
$this->assertInstanceOf(AbstractArgument::class, $arguments['$baz']);
}
}