Description
While delivering a training on Symfony 3.1 last week, my students and I stumbled upon an unexpected behavior. Some private services we defined were public in the end in the final dumped container. It was possible to request them outside of the container thanks to its get()
method.
Consider the following scenarii.
Use Case 1: Orphan Private Service is Removed
services:
app.foo:
class: Foo
public: false
This use case works as expected. An orphan private service is always removed from the final dumped container. It can never be requested to the container.
Use Case 2 : Private Service Referenced Once is Inlined
services:
app.bar:
class: Bar
arguments: ['@app.foo']
app.foo:
class: Foo
public: false
This use case also works as expected. The private app.foo
service is inlined in the generated getApp_BarService()
method in the compiled container. The app.foo
service cannot be requested outside of the container because it's inlined.
Use Case 3 : Private Service Referenced More Than Once is Shared and Public
This is the problem! Consider the following definition.
services:
app.baz:
class: Baz
arguments: ['@app.foo']
app.bar:
class: Bar
arguments: ['@app.foo']
app.foo:
class: Foo
public: false
In this situation, the private service app.foo
is referenced twice by two public services. As such the compiled container generates a protected getApp_FooService()
method that will create the app.foo
service on demand when its requested with the Container::get()
method. The app.foo
service reference will be stored in the $services
array of the dumped container.
It's also possible to request this service outside of the container:
$container->get('app.foo');
The final dumped container looks like the following in my Symfony 3.1 SE project:
<?php
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* appDevDebugProjectContainer.
*
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*/
class appDevDebugProjectContainer extends Container
{
private $parameters;
private $targetDirs = array();
/**
* Constructor.
*/
public function __construct()
{
$dir = __DIR__;
for ($i = 1; $i <= 5; ++$i) {
$this->targetDirs[$i] = $dir = dirname($dir);
}
$this->parameters = $this->getDefaultParameters();
$this->services = array();
$this->methodMap = array(
'annotation_reader' => 'getAnnotationReaderService',
'app.bar' => 'getApp_BarService',
'app.baz' => 'getApp_BazService',
'app.foo' => 'getApp_FooService',
// ...
);
// ...
}
// ...
/**
* Gets the 'app.foo' 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 \Foo A Foo instance.
*/
protected function getApp_FooService()
{
return $this->services['app.foo'] = new \Foo();
}
/**
* Gets the 'app.bar' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return \Bar A Bar instance.
*/
protected function getApp_BarService()
{
return $this->services['app.bar'] = new \Bar($this->get('app.foo'));
}
/**
* Gets the 'app.baz' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* @return \Baz A Baz instance.
*/
protected function getApp_BazService()
{
return $this->services['app.baz'] = new \Baz($this->get('app.foo'));
}
}