added a new Syntax to define factories as callables.

This commit is contained in:
Rouven Weßling 2014-04-13 17:18:39 +02:00 committed by Fabien Potencier
parent 4ee2e93109
commit bd8531d2d8
29 changed files with 320 additions and 16 deletions

View File

@ -86,6 +86,11 @@ UPGRADE FROM 2.x to 3.0
$table->render();
```
### DependencyInjection
* The methods `setFactoryClass()`, `setFactoryMethod()` and `setFactoryService()` have been removed in favor of `setFactory()`.
Services defined using YAML or XML use the same syntax as configurators.
### EventDispatcher
* The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface`

View File

@ -1,6 +1,11 @@
CHANGELOG
=========
2.6.0
-----
* added new factory syntax and deprecated the old one
2.5.0
-----

View File

@ -81,6 +81,9 @@ class AnalyzeServiceReferencesPass implements RepeatablePassInterface
if ($definition->getConfigurator()) {
$this->processArguments(array($definition->getConfigurator()));
}
if ($definition->getFactory()) {
$this->processArguments(array($definition->getFactory()));
}
}
}

View File

@ -56,9 +56,16 @@ class CheckDefinitionValidityPass implements CompilerPassInterface
));
}
if ($definition->getFactory() && ($definition->getFactoryClass() || $definition->getFactoryService() || $definition->getFactoryMethod())) {
throw new RuntimeException(sprintf(
'A service ("%s") can use either the old or the new factory syntax, not both.',
$id
));
}
// non-synthetic, non-abstract service has class
if (!$definition->isAbstract() && !$definition->isSynthetic() && !$definition->getClass()) {
if ($definition->getFactoryClass() || $definition->getFactoryService()) {
if ($definition->getFactory() || $definition->getFactoryClass() || $definition->getFactoryService()) {
throw new RuntimeException(sprintf(
'Please add the class to service "%s" even if it is constructed by a factory '
.'since we might need to add method calls based on compile-time checks.',

View File

@ -67,6 +67,11 @@ class InlineServiceDefinitionsPass implements RepeatablePassInterface
$definition->setConfigurator(
$configurator[0]
);
$factory = $this->inlineArguments($container, array($definition->getFactory()));
$definition->setFactory(
$factory[0]
);
}
}

View File

@ -103,6 +103,9 @@ class ResolveDefinitionTemplatesPass implements CompilerPassInterface
if (isset($changes['factory_service'])) {
$def->setFactoryService($definition->getFactoryService());
}
if (isset($changes['factory'])) {
$def->setFactory($definition->getFactory());
}
if (isset($changes['configurator'])) {
$def->setConfigurator($definition->getConfigurator());
}

View File

@ -25,6 +25,7 @@ class Definition
{
private $class;
private $file;
private $factory;
private $factoryClass;
private $factoryMethod;
private $factoryService;
@ -56,6 +57,34 @@ class Definition
$this->arguments = $arguments;
}
/**
* Sets a factory
*
* @param callable $factory The PHP callable to call or an array containing a Reference and a method to call
*
* @return Definition The current instance
*
* @api
*/
public function setFactory($factory)
{
$this->factory = $factory;
return $this;
}
/**
* Gets the factory .
*
* @return callable|array The PHP callable to call or an array containing a Reference and a method to call
*
* @api
*/
public function getFactory()
{
return $this->factory;
}
/**
* Sets the name of the class that acts as a factory using the factory method,
* which will be invoked statically.
@ -65,6 +94,7 @@ class Definition
* @return Definition The current instance
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function setFactoryClass($factoryClass)
{
@ -79,6 +109,7 @@ class Definition
* @return string|null The factory class name
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function getFactoryClass()
{
@ -93,6 +124,7 @@ class Definition
* @return Definition The current instance
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function setFactoryMethod($factoryMethod)
{
@ -142,6 +174,7 @@ class Definition
* @return string|null The factory method name
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function getFactoryMethod()
{
@ -156,6 +189,7 @@ class Definition
* @return Definition The current instance
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function setFactoryService($factoryService)
{
@ -170,6 +204,7 @@ class Definition
* @return string|null The factory service id
*
* @api
* @deprecated Deprecated since version 2.5, to be removed in 3.0.
*/
public function getFactoryService()
{

View File

@ -81,6 +81,18 @@ class DefinitionDecorator extends Definition
*
* @api
*/
public function setFactory($callable)
{
$this->changes['factory'] = true;
return parent::setFactory($callable);
}
/**
* {@inheritDoc}
*
* @api
*/
public function setFactoryClass($class)
{
$this->changes['factory_class'] = true;

View File

@ -161,6 +161,7 @@ class PhpDumper extends Dumper
$this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior);
$this->getServiceCallsFromArguments($iDefinition->getProperties(), $calls, $behavior);
$this->getServiceCallsFromArguments(array($iDefinition->getConfigurator()), $calls, $behavior);
$this->getServiceCallsFromArguments(array($iDefinition->getFactory()), $calls, $behavior);
}
$code = '';
@ -523,6 +524,17 @@ class PhpDumper extends Dumper
$return[] = '@throws RuntimeException always since this service is expected to be injected dynamically';
} elseif ($class = $definition->getClass()) {
$return[] = sprintf("@return %s A %s instance.", 0 === strpos($class, '%') ? 'object' : "\\".$class, $class);
} elseif ($definition->getFactory()) {
$factory = $definition->getFactory();
if (is_string($factory)) {
$return[] = sprintf('@return object An instance returned by %s().', $factory);
} elseif (is_array($factory) && (is_string($factory[0]) || $factory[0] instanceof Definition || $factory[0] instanceof Reference)) {
if (is_string($factory[0] || $factory[0] instanceof Reference)) {
$return[] = sprintf('@return object An instance returned by %s::%s().', (string) $factory[0], $factory[1]);
} elseif ($factory[0] instanceof Definition) {
$return[] = sprintf('@return object An instance returned by %s::%s().', $factory[0]->getClass(), $factory[1]);
}
}
} elseif ($definition->getFactoryClass()) {
$return[] = sprintf('@return object An instance returned by %s::%s().', $definition->getFactoryClass(), $definition->getFactoryMethod());
} elseif ($definition->getFactoryService()) {
@ -701,7 +713,26 @@ EOF;
$arguments[] = $this->dumpValue($value);
}
if (null !== $definition->getFactoryMethod()) {
if (null !== $definition->getFactory()) {
$callable = $definition->getFactory();
if (is_array($callable)) {
if ($callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))) {
return sprintf(" $return{$instantiation}%s->%s(%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '');
}
$class = $this->dumpValue($callable[0]);
// If the class is a string we can optimize call_user_func away
if (strpos($class, "'") === 0) {
return sprintf(" $return{$instantiation}\%s::%s(%s);\n", substr($class, 1, -1), $callable[1], $arguments ? implode(', ', $arguments) : '');
}
return sprintf(" $return{$instantiation}call_user_func(array(%s, '%s'), %s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? ', '.implode(', ', $arguments) : '');
}
return sprintf(" $return{$instantiation}%s(%s);\n", $callable, $arguments ? implode(', ', $arguments) : '');
} elseif (null !== $definition->getFactoryMethod()) {
if (null !== $definition->getFactoryClass()) {
$class = $this->dumpValue($definition->getFactoryClass());
@ -1116,7 +1147,8 @@ EOF;
$this->getDefinitionsFromArguments($definition->getArguments()),
$this->getDefinitionsFromArguments($definition->getMethodCalls()),
$this->getDefinitionsFromArguments($definition->getProperties()),
$this->getDefinitionsFromArguments(array($definition->getConfigurator()))
$this->getDefinitionsFromArguments(array($definition->getConfigurator())),
$this->getDefinitionsFromArguments(array($definition->getFactory()))
);
$this->inlinedDefinitions->offsetSet($definition, $definitions);

View File

@ -176,6 +176,17 @@ class XmlDumper extends Dumper
$this->addMethodCalls($definition->getMethodCalls(), $service);
if ($callable = $definition->getFactory()) {
$factory = $this->document->createElement('factory');
if (is_array($callable)) {
$factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]);
$factory->setAttribute('method', $callable[1]);
} else {
$factory->setAttribute('function', $callable);
}
$service->appendChild($factory);
}
if ($callable = $definition->getConfigurator()) {
$configurator = $this->document->createElement('configurator');
if (is_array($callable)) {

View File

@ -147,16 +147,12 @@ class YamlDumper extends Dumper
}
}
if ($callable = $definition->getConfigurator()) {
if (is_array($callable)) {
if ($callable[0] instanceof Reference) {
$callable = array($this->getServiceCall((string) $callable[0], $callable[0]), $callable[1]);
} else {
$callable = array($callable[0], $callable[1]);
}
}
if ($callable = $definition->getFactory()) {
$code .= sprintf(" factory: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
}
$code .= sprintf(" configurator: %s\n", $this->dumper->dump($callable, 0));
if ($callable = $definition->getConfigurator()) {
$code .= sprintf(" configurator: %s\n", $this->dumper->dump($this->dumpCallable($callable), 0));
}
return $code;
@ -222,6 +218,26 @@ class YamlDumper extends Dumper
return $this->dumper->dump(array('parameters' => $parameters), 2);
}
/**
* Dumps callable to YAML format
*
* @param callable $callable
*
* @return callable
*/
private function dumpCallable($callable)
{
if (is_array($callable)) {
if ($callable[0] instanceof Reference) {
$callable = array($this->getServiceCall((string) $callable[0], $callable[0]), $callable[1]);
} else {
$callable = array($callable[0], $callable[1]);
}
}
return $callable;
}
/**
* Dumps the value to YAML format
*

View File

@ -167,6 +167,21 @@ class XmlFileLoader extends FileLoader
$definition->setArguments($this->getArgumentsAsPhp($service, 'argument'));
$definition->setProperties($this->getArgumentsAsPhp($service, 'property'));
if ($factories = $this->getChildren($service, 'factory')) {
$factory = $factories[0];
if ($function = $factory->getAttribute('function')) {
$definition->setFactory($function);
} else {
if ($childService = $factory->getAttribute('service')) {
$class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false);
} else {
$class = $factory->getAttribute('class');
}
$definition->setFactory(array($class, $factory->getAttribute('method')));
}
}
if ($configurators = $this->getChildren($service, 'configurator')) {
$configurator = $configurators[0];
if ($function = $configurator->getAttribute('function')) {

View File

@ -194,6 +194,19 @@ class YamlFileLoader extends FileLoader
$definition->setAbstract($service['abstract']);
}
if (isset($service['factory'])) {
if (is_string($service['factory'])) {
if (strpos($service['factory'], ':')) {
$parts = explode(':', $service['factory']);
$definition->setFactory(array($this->resolveServices('@'.$parts[0]), $parts[1]));
} else {
$definition->setFactory($service['factory']);
}
} else {
$definition->setFactory(array($this->resolveServices($service['factory'][0]), $service['factory'][1]));
}
}
if (isset($service['factory_class'])) {
$definition->setFactoryClass($service['factory_class']);
}

View File

@ -64,7 +64,7 @@
<xsd:attribute name="ignore-errors" type="boolean" />
</xsd:complexType>
<xsd:complexType name="configurator">
<xsd:complexType name="callable">
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="service" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
@ -76,7 +76,8 @@
<xsd:choice maxOccurs="unbounded">
<xsd:element name="file" type="xsd:string" minOccurs="0" maxOccurs="1" />
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="configurator" type="configurator" minOccurs="0" maxOccurs="1" />
<xsd:element name="configurator" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="factory" type="callable" minOccurs="0" maxOccurs="1" />
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />

View File

@ -38,8 +38,7 @@ If your service is retrieved by calling a static method:
$sc
->register('bar', '%bar.class%')
->setFactoryClass('%bar.class%')
->setFactoryMethod('getInstance')
->setFactory(array('%bar.class%', 'getInstance'))
->addArgument('Aarrg!!!')
;
$sc->setParameter('bar.class', 'Bar');

View File

@ -50,6 +50,17 @@ class CheckDefinitionValidityPassTest extends \PHPUnit_Framework_TestCase
$this->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
*/
public function testProcessDetectsBothFactorySyntaxesUsed()
{
$container = new ContainerBuilder();
$container->register('a')->setFactory(array('a', 'b'))->setFactoryClass('a');
$this->process($container);
}
public function testProcess()
{
$container = new ContainerBuilder();

View File

@ -43,6 +43,7 @@ class DefinitionDecoratorTest extends \PHPUnit_Framework_TestCase
{
return array(
array('class', 'class'),
array('factory', 'factory'),
array('factoryClass', 'factory_class'),
array('factoryMethod', 'factory_method'),
array('factoryService', 'factory_service'),

View File

@ -27,6 +27,17 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('foo'), $def->getArguments(), '__construct() takes an optional array of arguments as its second argument');
}
/**
* @covers Symfony\Component\DependencyInjection\Definition::setFactory
* @covers Symfony\Component\DependencyInjection\Definition::getFactory
*/
public function testSetGetFactory()
{
$def = new Definition('stdClass');
$this->assertSame($def, $def->setFactory('foo'), '->setFactory() implements a fluent interface');
$this->assertEquals('foo', $def->getFactory(), '->getFactory() returns the factory');
}
public function testSetGetFactoryClass()
{
$def = new Definition('stdClass');

View File

@ -103,5 +103,16 @@ $container
->register('decorator_service_with_name', 'stdClass')
->setDecoratedService('decorated', 'decorated.pif-pouf')
;
$container
->register('new_factory', 'FactoryClass')
->setProperty('foo', 'bar')
->setScope('container')
->setPublic(false)
;
$container
->register('new_factory_service', 'FooBarBaz')
->setProperty('foo', 'bar')
->setFactory(array(new Reference('new_factory'), 'getInstance'))
;
return $container;

View File

@ -19,6 +19,8 @@ digraph sc {
node_decorated [label="decorated\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_decorator_service [label="decorator_service\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_decorator_service_with_name [label="decorator_service_with_name\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_new_factory [label="new_factory\nFactoryClass\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_new_factory_service [label="new_factory_service\nFooBarBaz\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"];
node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"];
node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"];

View File

@ -38,6 +38,8 @@ class ProjectServiceContainer extends Container
'foo_with_inline' => 'getFooWithInlineService',
'inlined' => 'getInlinedService',
'method_call1' => 'getMethodCall1Service',
'new_factory' => 'getNewFactoryService',
'new_factory_service' => 'getNewFactoryServiceService',
'request' => 'getRequestService',
);
$this->aliases = array(
@ -265,6 +267,23 @@ class ProjectServiceContainer extends Container
return $instance;
}
/**
* Gets the 'new_factory_service' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return \FooBarBaz A FooBarBaz instance.
*/
protected function getNewFactoryServiceService()
{
$this->services['new_factory_service'] = $instance = $this->get('new_factory')->getInstance();
$instance->foo = 'bar';
return $instance;
}
/**
* Gets the 'request' service.
*
@ -331,6 +350,27 @@ class ProjectServiceContainer extends Container
return $instance;
}
/**
* Gets the 'new_factory' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* This service is private.
* If you want to be able to request this service from the container directly,
* make it public, otherwise you might end up with broken code.
*
* @return \FactoryClass A FactoryClass instance.
*/
protected function getNewFactoryService()
{
$this->services['new_factory'] = $instance = new \FactoryClass();
$instance->foo = 'bar';
return $instance;
}
/**
* Gets the default parameters.
*

View File

@ -46,6 +46,7 @@ class ProjectServiceContainer extends Container
'foo_bar' => 'getFooBarService',
'foo_with_inline' => 'getFooWithInlineService',
'method_call1' => 'getMethodCall1Service',
'new_factory_service' => 'getNewFactoryServiceService',
'request' => 'getRequestService',
);
$this->aliases = array(
@ -269,6 +270,26 @@ class ProjectServiceContainer extends Container
return $instance;
}
/**
* Gets the 'new_factory_service' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return \FooBarBaz A FooBarBaz instance.
*/
protected function getNewFactoryServiceService()
{
$a = new \FactoryClass();
$a->foo = 'bar';
$this->services['new_factory_service'] = $instance = $a->getInstance();
$instance->foo = 'bar';
return $instance;
}
/**
* Gets the 'request' service.
*

View File

@ -52,5 +52,14 @@
<service id="request" class="Request" synthetic="true" synchronized="true" lazy="true"/>
<service id="decorator_service" decorates="decorated" />
<service id="decorator_service_with_name" decorates="decorated" decoration-inner-name="decorated.pif-pouf"/>
<service id="new_factory1" class="FooBarClass">
<factory function="factory" />
</service>
<service id="new_factory2" class="FooBarClass">
<factory service="baz" method="getClass" />
</service>
<service id="new_factory3" class="FooBarClass">
<factory class="BazClass" method="getInstance" />
</service>
</services>
</container>

View File

@ -91,6 +91,13 @@
<service id="decorated" class="stdClass"/>
<service id="decorator_service" class="stdClass" decorates="decorated"/>
<service id="decorator_service_with_name" class="stdClass" decorates="decorated" decoration-inner-name="decorated.pif-pouf"/>
<service id="new_factory" class="FactoryClass" public="false">
<property name="foo">bar</property>
</service>
<service id="new_factory_service" class="FooBarBaz">
<property name="foo">bar</property>
<factory service="new_factory" method="getInstance"/>
</service>
<service id="alias_for_foo" alias="foo"/>
<service id="alias_for_alias" alias="foo"/>
</services>

View File

@ -0,0 +1,2 @@
services:
factory: { class: FooBarClass, factory: baz:getClass}

View File

@ -35,3 +35,6 @@ services:
decorator_service_with_name:
decorates: decorated
decoration_inner_name: decorated.pif-pouf
new_factory1: { class: FooBarClass, factory: factory}
new_factory2: { class: FooBarClass, factory: [@baz, getClass]}
new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]}

View File

@ -88,5 +88,13 @@ services:
class: stdClass
decorates: decorated
decoration_inner_name: decorated.pif-pouf
new_factory:
class: FactoryClass
public: false
properties: { foo: bar }
new_factory_service:
class: FooBarBaz
properties: { foo: bar }
factory: ['@new_factory', getInstance]
alias_for_foo: @foo
alias_for_alias: @foo

View File

@ -216,6 +216,9 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertNull($services['factory_service']->getClass());
$this->assertEquals('getInstance', $services['factory_service']->getFactoryMethod());
$this->assertEquals('baz_factory', $services['factory_service']->getFactoryService());
$this->assertEquals('factory', $services['new_factory1']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(array(new Reference('baz', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false), 'getClass'), $services['new_factory2']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(array('BazClass', 'getInstance'), $services['new_factory3']->getFactory(), '->load() parses the factory tag');
$this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag');
$this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag');

View File

@ -141,6 +141,9 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$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());
$this->assertEquals('factory', $services['new_factory1']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(array(new Reference('baz'), 'getClass'), $services['new_factory2']->getFactory(), '->load() parses the factory tag');
$this->assertEquals(array('BazClass', 'getInstance'), $services['new_factory3']->getFactory(), '->load() parses the factory tag');
$this->assertTrue($services['request']->isSynthetic(), '->load() parses the synthetic flag');
$this->assertTrue($services['request']->isSynchronized(), '->load() parses the synchronized flag');
@ -159,6 +162,16 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('decorated', 'decorated.pif-pouf'), $services['decorator_service_with_name']->getDecoratedService());
}
public function testLoadFactoryShortSyntax()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services14.yml');
$services = $container->getDefinitions();
$this->assertEquals(array(new Reference('baz'), 'getClass'), $services['factory']->getFactory(), '->load() parses the factory tag');
}
public function testExtensions()
{
$container = new ContainerBuilder();