[DependencyInjection] added support for expressions in the service container

This commit is contained in:
Fabien Potencier 2013-09-02 12:46:47 +02:00
parent 3a41781640
commit c25abd9c72
21 changed files with 136 additions and 6 deletions

View File

@ -4,6 +4,7 @@ CHANGELOG
2.4.0
-----
* added support for expressions in service definitions
* added ContainerAwareTrait to add default container aware behavior to a class
2.2.0

View File

@ -24,6 +24,8 @@ use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\InstantiatorInterface;
use Symfony\Component\DependencyInjection\LazyProxy\Instantiator\RealServiceInstantiator;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* ContainerBuilder is a DI container that provides an API to easily describe services.
@ -78,6 +80,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/
private $proxyInstantiator;
/**
* @var ExpressionLanguage|null
*/
private $expressionLanguage;
/**
* Sets the track resources flag.
*
@ -983,11 +990,12 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
}
/**
* Replaces service references by the real service instance.
* Replaces service references by the real service instance and evaluates expressions.
*
* @param mixed $value A value
*
* @return mixed The same value with all service references replaced by the real service instances
* @return mixed The same value with all service references replaced by
* the real service instances and all expressions evaluated
*/
public function resolveServices($value)
{
@ -999,6 +1007,8 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$value = $this->get((string) $value, $value->getInvalidBehavior());
} elseif ($value instanceof Definition) {
$value = $this->createService($value, null);
} elseif ($value instanceof Expression) {
$value = $this->getExpressionLanguage()->evaluate($value, array('this' => $this));
}
return $value;
@ -1149,4 +1159,16 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
}
}
}
private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage();
}
return $this->expressionLanguage;
}
}

View File

@ -23,6 +23,8 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper;
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\NullDumper;
use Symfony\Component\DependencyInjection\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* PhpDumper dumps a service container as a PHP class.
@ -51,6 +53,7 @@ class PhpDumper extends Dumper
private $referenceVariables;
private $variableCount;
private $reservedVariables = array('instance', 'class');
private $expressionLanguage;
/**
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
@ -1197,6 +1200,8 @@ EOF;
}
return $this->getServiceCall((string) $value, $value);
} elseif ($value instanceof Expression) {
return $this->getExpressionLanguage()->compile((string) $value, array('this'));
} elseif ($value instanceof Parameter) {
return $this->dumpParameter($value);
} elseif (true === $interpolate && is_string($value)) {
@ -1319,4 +1324,16 @@ EOF;
return $name;
}
}
private function getExpressionLanguage()
{
if (null === $this->expressionLanguage) {
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
throw new RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
}
$this->expressionLanguage = new ExpressionLanguage();
}
return $this->expressionLanguage;
}
}

View File

@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* XmlDumper dumps a service container as an XML string.
@ -259,6 +260,10 @@ class XmlDumper extends Dumper
} elseif ($value instanceof Definition) {
$element->setAttribute('type', 'service');
$this->addService($value, null, $element);
} elseif ($value instanceof Expression) {
$element->setAttribute('type', 'expression');
$text = $this->document->createTextNode(self::phpToXml((string) $value));
$element->appendChild($text);
} else {
if (in_array($value, array('null', 'true', 'false'), true)) {
$element->setAttribute('type', 'string');

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* YamlDumper dumps a service container as a YAML string.
@ -231,6 +232,8 @@ class YamlDumper extends Dumper
return $this->getServiceCall((string) $value, $value);
} elseif ($value instanceof Parameter) {
return $this->getParameterCall((string) $value);
} elseif ($value instanceof Expression) {
return $this->getExpressionCall((string) $value);
} 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.');
}
@ -267,6 +270,11 @@ class YamlDumper extends Dumper
return sprintf('%%%s%%', $id);
}
private function getExpressionCall($expression)
{
return sprintf('@=%s', $expression);
}
/**
* Prepares parameters.
*

View File

@ -0,0 +1,42 @@
<?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;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;
/**
* Adds some function to the default ExpressionLanguage.
*
* To get a service, use service('request').
* To get a parameter, use parameter('kernel.debug').
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionLanguage extends BaseExpressionLanguage
{
protected function registerFunctions()
{
parent::registerFunctions();
$this->addFunction('service', function ($arg) {
return sprintf('$this->get(%s)', $arg);
}, function (array $variables, $value) {
return $variables['container']->get($value);
});
$this->addFunction('parameter', function ($arg) {
return sprintf('$this->getParameter(%s)', $arg);
}, function (array $variables, $value) {
return $variables['container']->getParameter($value);
});
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* YamlFileLoader loads YAML files service definitions.
@ -311,6 +312,8 @@ class YamlFileLoader extends FileLoader
{
if (is_array($value)) {
$value = array_map(array($this, 'resolveServices'), $value);
} elseif (is_string($value) && 0 === strpos($value, '@=')) {
return new Expression(substr($value, 2));
} elseif (is_string($value) && 0 === strpos($value, '@')) {
if (0 === strpos($value, '@@')) {
$value = substr($value, 1);

View File

@ -164,6 +164,7 @@
<xsd:restriction base="xsd:string">
<xsd:enumeration value="collection" />
<xsd:enumeration value="service" />
<xsd:enumeration value="expression" />
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
</xsd:restriction>

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\ExpressionLanguage\Expression;
/**
* SimpleXMLElement class.
@ -77,6 +78,9 @@ class SimpleXMLElement extends \SimpleXMLElement
$arguments[$key] = new Reference((string) $arg['id'], $invalidBehavior, $strict);
break;
case 'expression':
$arguments[$key] = new Expression((string) $arg);
break;
case 'collection':
$arguments[$key] = $arg->getArgumentsAsPhp($name, false);
break;

View File

@ -25,6 +25,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Scope;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\ExpressionLanguage\Expression;
class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
{
@ -377,6 +378,15 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$builder->get('foo');
}
public function testCreateServiceWithExpression()
{
$builder = new ContainerBuilder();
$builder->setParameter('bar', 'bar');
$builder->register('bar', 'BarClass');
$builder->register('foo', 'FooClass')->addArgument(array('foo' => new Expression('service("bar").foo ~ parameter("bar")')));
$this->assertEquals('foobar', $builder->get('foo')->arguments['foo']);
}
/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::resolveServices
*/
@ -386,6 +396,7 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$builder->register('foo', 'FooClass');
$this->assertEquals($builder->get('foo'), $builder->resolveServices(new Reference('foo')), '->resolveServices() resolves service references to service instances');
$this->assertEquals(array('foo' => array('foo', $builder->get('foo'))), $builder->resolveServices(array('foo' => array('foo', new Reference('foo')))), '->resolveServices() resolves service references to service instances in nested arrays');
$this->assertEquals($builder->get('foo'), $builder->resolveServices(new Expression('service("foo")')), '->resolveServices() resolves expressions');
}
/**

View File

@ -6,6 +6,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\ExpressionLanguage\Expression;
$container = new ContainerBuilder();
$container->
@ -50,7 +51,8 @@ $container->
addMethodCall('setBar', array(new Reference('foo')))->
addMethodCall('setBar', array(new Reference('foo2', ContainerInterface::NULL_ON_INVALID_REFERENCE)))->
addMethodCall('setBar', array(new Reference('foo3', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))->
addMethodCall('setBar', array(new Reference('foobaz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))
addMethodCall('setBar', array(new Reference('foobaz', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))->
addMethodCall('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")')))
;
$container->
register('factory_service', 'Bar')->

View File

@ -8,6 +8,7 @@ function sc_configure($instance)
class BarClass
{
protected $baz;
public $foo = 'foo';
public function setBaz(BazClass $baz)
{

View File

@ -198,6 +198,7 @@ class ProjectServiceContainer extends Container
if ($this->has('foobaz')) {
$instance->setBar($this->get('foobaz', ContainerInterface::NULL_ON_INVALID_REFERENCE));
}
$instance->setBar(($this->get("foo")->foo() . $this->getParameter("foo")));
return $instance;
}

View File

@ -203,6 +203,7 @@ class ProjectServiceContainer extends Container
$instance->setBar($this->get('foo'));
$instance->setBar(NULL);
$instance->setBar(($this->get("foo")->foo() . $this->getParameter("foo")));
return $instance;
}

View File

@ -32,6 +32,9 @@
</service>
<service id="method_call1" class="FooClass">
<call method="setBar" />
<call method="setBar">
<argument type="expression">service("foo").foo() ~ parameter("foo")</argument>
</call>
</service>
<service id="method_call2" class="FooClass">
<call method="setBar">

View File

@ -49,6 +49,9 @@
<call method="setBar">
<argument type="service" id="foobaz" on-invalid="ignore"/>
</call>
<call method="setBar">
<argument type="expression">service("foo").foo() ~ parameter("foo")</argument>
</call>
</service>
<service id="factory_service" class="Bar" factory-method="getInstance" factory-service="foo.baz"/>
<service id="foo_with_inline" class="Foo">

View File

@ -15,6 +15,7 @@ services:
calls:
- [ setBar, [] ]
- [ setBar ]
- [ setBar, ['@=service("foo").foo() ~ parameter("foo")'] ]
method_call2:
class: FooClass
calls:

View File

@ -38,6 +38,7 @@ services:
- [setBar, ['@?foo2']]
- [setBar, ['@?foo3']]
- [setBar, ['@?foobaz']]
- [setBar, ['@=service("foo").foo() ~ parameter("foo")']]
factory_service:
class: Bar

View File

@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\ExpressionLanguage\Expression;
class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
{
@ -211,7 +212,7 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(array('setBar', array())), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals(array(array('setBar', array()), array('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")')))), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertNull($services['factory_service']->getClass());
$this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod());

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Loader\IniFileLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\ExpressionLanguage\Expression;
class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
{
@ -113,7 +114,7 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('sc_configure', $services['configurator1']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(new Reference('baz'), 'configure'), $services['configurator2']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array('BazClass', 'configureStatic'), $services['configurator3']->getConfigurator(), '->load() parses the configurator tag');
$this->assertEquals(array(array('setBar', array()), array('setBar', array())), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals(array(array('setBar', array()), array('setBar', array()), array('setBar', array(new Expression('service("foo").foo() ~ parameter("foo")')))), $services['method_call1']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals(array(array('setBar', array('foo', new Reference('foo'), array(true, false)))), $services['method_call2']->getMethodCalls(), '->load() parses the method_call tag');
$this->assertEquals('baz_factory', $services['factory_service']->getFactoryService());

View File

@ -20,7 +20,8 @@
},
"require-dev": {
"symfony/yaml": "~2.0",
"symfony/config": "~2.2"
"symfony/config": "~2.2",
"symfony/expression-language": "~2.4"
},
"suggest": {
"symfony/yaml": "",