Description
The DIC v2.6 introduced Definition::setFactory()
(and corresponding config syntax) and deprecated setFactoryService()
, setFactoryMethod()
etc.
If one injects a private service (for use as factory) using the new method, and then compiles the container, the resulting container definition cannot be dumped with Symfony\Component\DependencyInjection\Dumper\XmlDumper
. For example:
services:
factory_service:
class: Some\Factory
public: false
my_service:
class: MyClass
factory: [ '@factory_service', 'getThing' ]
Compiling a container built with this config, then attempting to dump...
$container->compile();
$dumper = new Symfony\Component\DependencyInjection\Dumper\XmlDumper($container);
$dumper->dump();
...throws the following ContextErrorException
:
Warning: DOMElement::setAttribute() expects parameter 2 to be string, object given
If one reverts to the old config syntax/Definition setter methods, there is no issue. The following config results in a dumpable, compiled container:
services:
factory_service:
class: Some\Factory
public: false
my_service:
class: MyClass
factory_service: factory_service
factory_method: getThing
I dug into the problem and concluded that the compiler is inlining service factories only if they are configured using the new method. It will skip inlining if they are set with the old methods. The reason for this lies inside Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass::process
- it calls inlineArguments()
on Definition::getFactory()
, but there is no corresponding inlineArguments()
call on Definition::getServiceFactory()
. It would appear that inlined factory services cannot be dumped, because the compiler pass results in the "my_service" Definition object having a Reference object swapped for a Definition. If we print_r()
the compiled container's definitions property...
Inlined (uses setFactory()
):
[my_service] => Symfony\Component\DependencyInjection\Definition Object
(
[class:Symfony\Component\DependencyInjection\Definition:private] => MyClass
[file:Symfony\Component\DependencyInjection\Definition:private] =>
[factory:Symfony\Component\DependencyInjection\Definition:private] => Array
(
[0] => Symfony\Component\DependencyInjection\Definition Object
(
[class:Symfony\Component\DependencyInjection\Definition:private] => Some\Factory
Not inlined (uses setFactoryService()
):
[my_service] => Symfony\Component\DependencyInjection\Definition Object
(
[class:Symfony\Component\DependencyInjection\Definition:private] => MyClass
[file:Symfony\Component\DependencyInjection\Definition:private] =>
[factory:Symfony\Component\DependencyInjection\Definition:private] =>
[factoryClass:Symfony\Component\DependencyInjection\Definition:private] =>
[factoryMethod:Symfony\Component\DependencyInjection\Definition:private] => factoryMethod
[factoryService:Symfony\Component\DependencyInjection\Definition:private] => Symfony\Component\DependencyInjection\Reference Object
(
[id:Symfony\Component\DependencyInjection\Reference:private] => factory_service
[invalidBehavior:Symfony\Component\DependencyInjection\Reference:private] => 1
[strict:Symfony\Component\DependencyInjection\Reference:private] => 1
)
The stacktrace of the exception shows that, during XmlDumper::addService()
the factory
or factoryService
object is passed into a DOMElement::setAttribute()
call, and therefore must be castable to a string. There is no __toString()
method available on Symfony\Component\DependencyInjection\Definition
...
It's worth noting that there is no issue with using the "my_service" itself, after compilation. The optimised, inline factory service is fully usable within the container. This is not a show-stopper for projects using the standalone DIC. However, full-stack symfony projects have an issue: XmlDumper::dump
is called by the Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass
, which is difficult to circumvent, and such an act would be undesirable anyway as debugging container config is a normal part of the development process. In order to mitigate at present: either all factory services must be public, or used multiple times, or defined using the deprecated functionality.
For what my opinion is worth (probably not much), I would argue that in-lining of factory services should be disabled until the config syntax is expressive enough to support the resulting compiled container definition: a factory service likely has its own dependencies configured, and these cannot be represented in the current yaml/xml format if they are switched to inline. This is because only factory method injection is currently supported.
For example:
services:
factory_service:
class: Some\Factory
public: false
my_service:
class: MyClass
factory: [ '@factory_service', 'getThing' ]
Could be represented as
services:
my_service:
class: MyClass
factory: [ 'Some\Factory', 'getThing' ]
However, despite the fact that factory service dependencies will be preserved in the compiled container, it is not possible to represent the factory's own constructor/setter dependencies using the inline config form. The following cannot be represented in inline config, because there would be no way to inject the constructor argument:
services:
factory_service:
class: Some\Factory
arguments: ['Some\Vague\HelperClass']
public: false
my_service:
class: MyClass
factory: [ '@factory_service', 'getThing' ]
Finally, I had brief look at how YamlDumper behaves in the above scenario: although YamlDumper does not throw an exception when dumping the compiled container, it is setting the factory array's class parameter to "null"...