diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index f33e9fa0450ce..a29556e861543 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -65,6 +65,10 @@ public function __construct() )), new CheckExceptionOnInvalidReferenceBehaviorPass(), )); + + $this->afterRemovingPasses = array(array( + new RandomizePrivateServiceIdentifiers(), + )); } /** diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RandomizePrivateServiceIdentifiers.php b/src/Symfony/Component/DependencyInjection/Compiler/RandomizePrivateServiceIdentifiers.php new file mode 100644 index 0000000000000..d472a66a62936 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/RandomizePrivateServiceIdentifiers.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Randomizes private service identifiers, effectively making them unaccessable from outside. + * + * @author Roland Franssen + */ +class RandomizePrivateServiceIdentifiers implements CompilerPassInterface +{ + private $idMap; + private $randomizer; + + public function process(ContainerBuilder $container) + { + // update private definitions + build id map + $this->idMap = array(); + foreach ($container->getDefinitions() as $id => $definition) { + if (!$definition->isPublic()) { + $this->idMap[$id] = $this->randomizer ? (string) call_user_func($this->randomizer, $id) : hash('sha256', mt_rand().$id); + $definition->setPublic(true); + } + } + + // rename definitions + $aliases = $container->getAliases(); + foreach ($this->idMap as $oldId => $newId) { + $container->setDefinition($newId, $container->getDefinition($oldId)); + $container->removeDefinition($oldId); + } + + // update referencing definitions + foreach ($container->getDefinitions() as $id => $definition) { + $definition->setArguments($this->processArguments($definition->getArguments())); + $definition->setMethodCalls($this->processArguments($definition->getMethodCalls())); + $definition->setProperties($this->processArguments($definition->getProperties())); + $definition->setFactory($this->processFactory($definition->getFactory())); + if (null !== ($decorated = $definition->getDecoratedService()) && isset($this->idMap[$decorated[0]])) { + $definition->setDecoratedService($this->idMap[$decorated[0]], $decorated[1], $decorated[2]); + } + } + + // update alias map + $aliases = $container->getAliases(); + foreach ($container->getAliases() as $oldId => $alias) { + if(isset($this->idMap[$oldId]) && $oldId === (string) $alias) { + $container->setAlias(new Alias($this->idMap[$oldId], $alias->isPublic()), $this->idMap[$oldId]); + } + } + + // for BC + $reflectionProperty = new \ReflectionProperty(ContainerBuilder::class, 'privates'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($container, $this->idMap); + } + + public function setRandomizer(callable $randomizer = null) + { + $this->randomizer = $randomizer; + } + + private function processArguments(array $arguments) + { + foreach ($arguments as $k => $argument) { + if (is_array($argument)) { + $arguments[$k] = $this->processArguments($argument); + } elseif ($argument instanceof Reference && isset($this->idMap[$id = (string) $argument])) { + $arguments[$k] = new Reference($this->idMap[$id], $argument->getInvalidBehavior()); + } + } + + return $arguments; + } + + private function processFactory($factory) + { + if (null === $factory || !is_array($factory) || !$factory[0] instanceof Reference) { + return $factory; + } + if (isset($this->idMap[$id = (string) $factory[0]])) { + $factory[0] = new Reference($this->idMap[$id], $factory[0]->getInvalidBehavior()); + } + + return $factory; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php index 0d7e9dafef483..345bf0f162f10 100644 --- a/src/Symfony/Component/DependencyInjection/Container.php +++ b/src/Symfony/Component/DependencyInjection/Container.php @@ -65,7 +65,7 @@ class Container implements ResettableContainerInterface protected $services = array(); protected $methodMap = array(); - protected $privates = array(); + protected $privates = array(); // for BC protected $aliases = array(); protected $loading = array(); @@ -172,19 +172,21 @@ public function set($id, $service) unset($this->aliases[$id]); } - $this->services[$id] = $service; - - if (null === $service) { - unset($this->services[$id]); - } - if (isset($this->privates[$id])) { if (null === $service) { @trigger_error(sprintf('Unsetting the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0.', $id), E_USER_DEPRECATED); + unset($this->privates[$id]); } else { @trigger_error(sprintf('Setting the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0. A new public service will be created instead.', $id), E_USER_DEPRECATED); + $id = $this->privates[$id]; } } + + if (null === $service) { + unset($this->services[$id]); + } else { + $this->services[$id] = $service; + } } /** @@ -209,6 +211,7 @@ public function has($id) } else { if (isset($this->privates[$id])) { @trigger_error(sprintf('Checking for the existence of the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0.', $id), E_USER_DEPRECATED); + $id = $this->privates[$id]; } return method_exists($this, 'get'.strtr($id, $this->underscoreMap).'Service'); @@ -263,6 +266,12 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE } elseif (method_exists($this, $method = 'get'.strtr($id, $this->underscoreMap).'Service')) { // $method is set to the right value, proceed } else { + if (isset($this->privates[$id])) { + @trigger_error(sprintf('Requesting the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0.', $id), E_USER_DEPRECATED); + + return $this->get($this->privates[$id]); + } + if (self::EXCEPTION_ON_INVALID_REFERENCE === $invalidBehavior) { if (!$id) { throw new ServiceNotFoundException($id); @@ -281,9 +290,6 @@ public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE return; } - if (isset($this->privates[$id])) { - @trigger_error(sprintf('Requesting the "%s" private service is deprecated since Symfony 3.2 and won\'t be supported anymore in Symfony 4.0.', $id), E_USER_DEPRECATED); - } $this->loading[$id] = true; @@ -339,9 +345,14 @@ public function reset() public function getServiceIds() { $ids = array(); + $reversedPrivates = array_flip($this->privates); foreach (get_class_methods($this) as $method) { if (preg_match('/^get(.+)Service$/', $method, $match)) { - $ids[] = self::underscore($match[1]); + $id = self::underscore($match[1]); + if (isset($reversedPrivates[$id])) { + $id = $reversedPrivates[$id]; + } + $ids[] = $id; } } $ids[] = 'service_container'; diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index cb3789165891a..13b049aa9cd77 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -542,9 +542,6 @@ public function compile() $this->compiled = true; foreach ($this->definitions as $id => $definition) { - if (!$definition->isPublic()) { - $this->privates[$id] = true; - } if ($this->trackResources && $definition->isLazy() && ($class = $definition->getClass()) && class_exists($class)) { $this->addClassResource(new \ReflectionClass($class)); } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 210b13eb84601..428259faf010c 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -839,6 +839,7 @@ public function __construct() $code .= "\n \$this->services = array();\n"; $code .= $this->addMethodMap(); + $code .= $this->addPrivateServices(); $code .= $this->addAliases(); $code .= <<<'EOF' @@ -892,6 +893,8 @@ private function addMethodMap() /** * Adds the privates property definition. * + * This method is intented for BC only. + * * @return string */ private function addPrivateServices() @@ -900,12 +903,13 @@ private function addPrivateServices() return ''; } + $reflectionProperty = new \ReflectionProperty(ContainerBuilder::class, 'privates'); + $reflectionProperty->setAccessible(true); + $code = ''; ksort($definitions); - foreach ($definitions as $id => $definition) { - if (!$definition->isPublic()) { - $code .= ' '.var_export($id, true)." => true,\n"; - } + foreach ($reflectionProperty->getValue($this->container) as $originId => $id) { + $code .= ' '.var_export($originId, true).' => '.var_export($id, true).",\n"; } if (empty($code)) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php index 28101c29fa984..93741de90aec5 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerTest.php @@ -126,7 +126,7 @@ public function testGetServiceIds() $sc = new ProjectServiceContainer(); $sc->set('foo', $obj = new \stdClass()); - $this->assertEquals(array('internal', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'service_container', 'foo'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods, followed by service ids defined by set()'); + $this->assertEquals(array('internal', 'sharing_internal', 'bar', 'foo_bar', 'foo.baz', 'circular', 'throw_exception', 'throws_exception_on_service_configuration', 'service_container', 'foo'), $sc->getServiceIds(), '->getServiceIds() returns defined service ids by getXXXService() methods, followed by service ids defined by set()'); } public function testSet() @@ -308,6 +308,15 @@ public function testThatCloningIsNotSupported() $this->assertTrue($clone->isPrivate()); } + public function testSharedPrivateService() + { + $c = new ProjectServiceContainer(); + $expected = new \stdClass(); + $expected->internal = new \stdClass(); + $this->assertTrue($c->has('sharing_internal')); + $this->assertEquals($expected, $c->get('sharing_internal'), '->get() returns a public service referencing a private service'); + } + /** * @group legacy * @requires function Symfony\Bridge\PhpUnit\ErrorAssert::assertDeprecationsAreTriggered @@ -379,6 +388,7 @@ class ProjectServiceContainer extends Container public $__foo_bar; public $__foo_baz; public $__internal; + public $__sharing_internal; public function __construct() { @@ -388,15 +398,24 @@ public function __construct() $this->__foo_bar = new \stdClass(); $this->__foo_baz = new \stdClass(); $this->__internal = new \stdClass(); - $this->privates = array('internal' => true); + $this->__sharing_internal = new \stdClass(); $this->aliases = array('alias' => 'bar'); + $this->privates = array('internal' => 'semirandom_internal'); } - protected function getInternalService() + protected function getSemirandomInternalService() { return $this->__internal; } + protected function getSharingInternalService() + { + $instance = $this->__sharing_internal; + $instance->internal = $this->get('semirandom_internal'); // simulates dumped behavior + + return $instance; + } + protected function getBarService() { return $this->__bar; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index d155a9ac763e2..09f69bb44c396 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper; +use Symfony\Component\DependencyInjection\Compiler\RandomizePrivateServiceIdentifiers; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -122,6 +123,13 @@ public function testAddService() // with compilation $container = include self::$fixturesPath.'/containers/container9.php'; + foreach ($container->getCompiler()->getPassConfig()->getPasses() as $pass) { + if ($pass instanceof RandomizePrivateServiceIdentifiers) { + $pass->setRandomizer(function ($id) { + return 'shared_private' === $id ? 'semirandom_'.$id : md5(uniqid($id)); + }); + } + } $container->compile(); $dumper = new PhpDumper($container); $this->assertEquals(str_replace('%path%', str_replace('\\', '\\\\', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR), file_get_contents(self::$fixturesPath.'/php/services9_compiled.php')), $dumper->dump(), '->dump() dumps services'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php index ba25dc3c99361..9f76165e45cff 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php @@ -129,5 +129,14 @@ ->register('factory_service_simple', 'Bar') ->setFactory(array(new Reference('factory_simple'), 'getInstance')) ; +$container + ->register('shared_private', 'stdClass') + ->setPublic(false); +$container + ->register('shared_private_dep1', 'stdClass') + ->setProperty('dep', new Reference('shared_private')); +$container + ->register('shared_private_dep2', 'stdClass') + ->setProperty('dep', new Reference('shared_private')); return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot index 3b24ef8ffbca3..f0e7d137aee4b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot @@ -26,6 +26,9 @@ digraph sc { node_service_from_static_method [label="service_from_static_method\nBar\\FooClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_factory_simple [label="factory_simple\nSimpleFactoryClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; node_factory_service_simple [label="factory_service_simple\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_shared_private [label="shared_private\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_shared_private_dep1 [label="shared_private_dep1\nstdClass\n", shape=record, fillcolor="#eeeeee", style="filled"]; + node_shared_private_dep2 [label="shared_private_dep2\nstdClass\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"]; @@ -43,4 +46,6 @@ digraph sc { node_inlined -> node_baz [label="setBaz()" style="dashed"]; node_baz -> node_foo_with_inline [label="setFoo()" style="dashed"]; node_configurator_service -> node_baz [label="setFoo()" style="dashed"]; + node_shared_private_dep1 -> node_shared_private [label="" style="dashed"]; + node_shared_private_dep2 -> node_shared_private [label="" style="dashed"]; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php index f77ae5dea1cdb..4648bb45d099c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php @@ -48,13 +48,9 @@ public function __construct() 'new_factory_service' => 'getNewFactoryServiceService', 'request' => 'getRequestService', 'service_from_static_method' => 'getServiceFromStaticMethodService', - ); - $this->privates = array( - 'configurator_service' => true, - 'configurator_service_simple' => true, - 'factory_simple' => true, - 'inlined' => true, - 'new_factory' => true, + 'shared_private' => 'getSharedPrivateService', + 'shared_private_dep1' => 'getSharedPrivateDep1Service', + 'shared_private_dep2' => 'getSharedPrivateDep2Service', ); $this->aliases = array( 'alias_for_alias' => 'foo', @@ -354,6 +350,40 @@ protected function getServiceFromStaticMethodService() return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance(); } + /** + * Gets the 'shared_private_dep1' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \stdClass A stdClass instance + */ + protected function getSharedPrivateDep1Service() + { + $this->services['shared_private_dep1'] = $instance = new \stdClass(); + + $instance->dep = $this->get('shared_private'); + + return $instance; + } + + /** + * Gets the 'shared_private_dep2' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \stdClass A stdClass instance + */ + protected function getSharedPrivateDep2Service() + { + $this->services['shared_private_dep2'] = $instance = new \stdClass(); + + $instance->dep = $this->get('shared_private'); + + return $instance; + } + /** * Gets the 'configurator_service' service. * @@ -452,6 +482,23 @@ protected function getNewFactoryService() return $instance; } + /** + * Gets the 'shared_private' 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 \stdClass A stdClass instance + */ + protected function getSharedPrivateService() + { + return $this->services['shared_private'] = new \stdClass(); + } + /** * Gets the default parameters. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php index bcbd4a67b6b9d..15a8ac248a465 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php @@ -43,7 +43,13 @@ public function __construct() 'method_call1' => 'getMethodCall1Service', 'new_factory_service' => 'getNewFactoryServiceService', 'request' => 'getRequestService', + 'semirandom_shared_private' => 'getSemirandomSharedPrivateService', 'service_from_static_method' => 'getServiceFromStaticMethodService', + 'shared_private_dep1' => 'getSharedPrivateDep1Service', + 'shared_private_dep2' => 'getSharedPrivateDep2Service', + ); + $this->privates = array( + 'shared_private' => 'semirandom_shared_private', ); $this->aliases = array( 'alias_for_alias' => 'foo', @@ -329,6 +335,19 @@ protected function getRequestService() throw new RuntimeException('You have requested a synthetic service ("request"). The DIC does not know how to construct this service.'); } + /** + * Gets the 'semirandom_shared_private' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \stdClass A stdClass instance + */ + protected function getSemirandomSharedPrivateService() + { + return $this->services['semirandom_shared_private'] = new \stdClass(); + } + /** * Gets the 'service_from_static_method' service. * @@ -342,6 +361,40 @@ protected function getServiceFromStaticMethodService() return $this->services['service_from_static_method'] = \Bar\FooClass::getInstance(); } + /** + * Gets the 'shared_private_dep1' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \stdClass A stdClass instance + */ + protected function getSharedPrivateDep1Service() + { + $this->services['shared_private_dep1'] = $instance = new \stdClass(); + + $instance->dep = $this->get('semirandom_shared_private'); + + return $instance; + } + + /** + * Gets the 'shared_private_dep2' service. + * + * This service is shared. + * This method always returns the same instance of the service. + * + * @return \stdClass A stdClass instance + */ + protected function getSharedPrivateDep2Service() + { + $this->services['shared_private_dep2'] = $instance = new \stdClass(); + + $instance->dep = $this->get('semirandom_shared_private'); + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml index 9ad09ad5918dd..3b7cca3736bc3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml @@ -115,6 +115,13 @@ + + + + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml index 44a174b6edb5b..6a615d480d5f0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml @@ -107,5 +107,14 @@ services: factory_service_simple: class: Bar factory: ['@factory_simple', getInstance] + shared_private: + class: stdClass + public: false + shared_private_dep1: + class: stdClass + properties: { dep: '@shared_private' } + shared_private_dep2: + class: stdClass + properties: { dep: '@shared_private' } alias_for_foo: '@foo' alias_for_alias: '@foo' diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index a351c62ebc881..119e17c43d580 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -220,7 +220,7 @@ public function testLoadAnonymousServices() $services = $container->getDefinitions(); $fooArgs = $services['foo']->getArguments(); $barArgs = $services['bar']->getArguments(); - $this->assertSame($fooArgs[0], $barArgs[0]); + $this->assertEquals($fooArgs[0], $barArgs[0]); } public function testLoadServices()