diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 541a540583..ce55ecf073 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -503,10 +503,6 @@ EOF; } } - if (false !== strpos($this->dumpLiteralClass($this->dumpValue($definition->getClass())), '$')) { - return false; - } - return true; } @@ -598,18 +594,16 @@ EOF; $return = array(); if ($class = $definition->getClass()) { - $class = $this->container->resolveEnvPlaceholders($class); + $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class); $return[] = sprintf(0 === strpos($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($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]); - } + $class = $factory[0] instanceof Definition ? $factory[0]->getClass() : (string) $factory[0]; + $class = $class instanceof Parameter ? '%'.$class.'%' : $this->container->resolveEnvPlaceholders($class); + $return[] = sprintf('@return object An instance returned by %s::%s()', $class, $factory[1]); } } @@ -704,7 +698,7 @@ EOF; if (\is_array($argument)) { $hasSelfRef = $this->addInlineVariables($head, $tail, $id, $argument, $forConstructor) || $hasSelfRef; } elseif ($argument instanceof Reference) { - $hasSelfRef = $this->addInlineReference($head, $tail, $id, $this->container->normalizeId($argument), $forConstructor) || $hasSelfRef; + $hasSelfRef = $this->addInlineReference($head, $id, $this->container->normalizeId($argument), $forConstructor) || $hasSelfRef; } elseif ($argument instanceof Definition) { $hasSelfRef = $this->addInlineService($head, $tail, $id, $argument, $forConstructor) || $hasSelfRef; } @@ -713,37 +707,31 @@ EOF; return $hasSelfRef; } - private function addInlineReference(&$head, &$tail, $id, $targetId, $forConstructor) + private function addInlineReference(&$code, $id, $targetId, $forConstructor) { + $hasSelfRef = isset($this->circularReferences[$id][$targetId]); + if ('service_container' === $targetId || isset($this->referenceVariables[$targetId])) { - return isset($this->circularReferences[$id][$targetId]); + return $hasSelfRef; } list($callCount, $behavior) = $this->serviceCalls[$targetId]; - if (2 > $callCount && (!$forConstructor || !isset($this->circularReferences[$id][$targetId]))) { - return isset($this->circularReferences[$id][$targetId]); + if (2 > $callCount && (!$hasSelfRef || !$forConstructor)) { + return $hasSelfRef; } $name = $this->getNextVariableName(); $this->referenceVariables[$targetId] = new Variable($name); $reference = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $behavior ? new Reference($targetId, $behavior) : null; - $code = sprintf(" \$%s = %s;\n", $name, $this->getServiceCall($targetId, $reference)); + $code .= sprintf(" \$%s = %s;\n", $name, $this->getServiceCall($targetId, $reference)); - if (!isset($this->circularReferences[$id][$targetId])) { - $head .= $code; - - return false; + if (!$hasSelfRef || !$forConstructor) { + return $hasSelfRef; } - if (!$forConstructor) { - $tail .= $code; - - return true; - } - - $head .= $code.sprintf(<<<'EOTXT' + $code .= sprintf(<<<'EOTXT' if (isset($this->%s['%s'])) { return $this->%1$s['%2$s']; @@ -766,7 +754,7 @@ EOTXT $arguments = array($definition->getArguments(), $definition->getFactory()); - if (2 > $this->inlinedDefinitions[$definition] && !$definition->getMethodCalls() && !$definition->getProperties() && !$definition->getConfigurator() && false === strpos($this->dumpValue($definition->getClass()), '$')) { + if (2 > $this->inlinedDefinitions[$definition] && !$definition->getMethodCalls() && !$definition->getProperties() && !$definition->getConfigurator()) { return $this->addInlineVariables($head, $tail, $id, $arguments, $forConstructor); } @@ -774,9 +762,13 @@ EOTXT $this->definitionVariables[$definition] = new Variable($name); $code = ''; - $hasSelfRef = $this->addInlineVariables($code, $tail, $id, $arguments, $forConstructor); + if ($forConstructor) { + $hasSelfRef = $this->addInlineVariables($code, $tail, $id, $arguments, $forConstructor); + } else { + $hasSelfRef = $this->addInlineVariables($code, $code, $id, $arguments, $forConstructor); + } $code .= $this->addNewInstance($definition, '$'.$name, ' = ', $id); - $hasSelfRef ? $tail .= ('' !== $tail ? "\n" : '').$code : $head .= ('' !== $head ? "\n" : '').$code; + $hasSelfRef && !$forConstructor ? $tail .= ('' !== $tail ? "\n" : '').$code : $head .= ('' !== $head ? "\n" : '').$code; $code = ''; $arguments = array($definition->getProperties(), $definition->getMethodCalls(), $definition->getConfigurator()); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 25761d28b0..3ffcf0dc0a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -863,6 +863,28 @@ class PhpDumperTest extends TestCase $this->assertEquals((object) array('p2' => (object) array('p3' => (object) array())), $container->get('foo')->bClone); } + public function testInlineSelfRef() + { + $container = new ContainerBuilder(); + + $bar = (new Definition('App\Bar')) + ->setProperty('foo', new Reference('App\Foo')); + + $baz = (new Definition('App\Baz')) + ->setProperty('bar', $bar) + ->addArgument($bar); + + $container->register('App\Foo') + ->setPublic(true) + ->addArgument($baz); + + $passConfig = $container->getCompiler()->getPassConfig(); + $container->compile(); + + $dumper = new PhpDumper($container); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_inline_self_ref.php', $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Inline_Self_Ref'))); + } + public function testHotPathOptimizations() { $container = include self::$fixturesPath.'/containers/container_inline_requires.php'; @@ -940,6 +962,18 @@ class PhpDumperTest extends TestCase $this->assertEquals((object) array('foo' => (object) array(123)), $container->get('bar')); } + public function testAdawsonContainer() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_adawson.yml'); + $container->compile(); + + $dumper = new PhpDumper($container); + $dump = $dumper->dump(); + $this->assertStringEqualsFile(self::$fixturesPath.'/php/services_adawson.php', $dumper->dump()); + } + /** * @group legacy * @expectedDeprecation The "private" service is private, getting it from the container is deprecated since Symfony 3.2 and will fail in 4.0. You should either make the service public, or stop using the container directly and use dependency injection instead. diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container19.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container19.php index 823a77f534..300a5cc769 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container19.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container19.php @@ -7,9 +7,12 @@ require_once __DIR__.'/../includes/classes.php'; $container = new ContainerBuilder(); +$container->setParameter('env(FOO)', 'Bar\FaooClass'); +$container->setParameter('foo', '%env(FOO)%'); + $container - ->register('service_from_anonymous_factory', 'Bar\FooClass') - ->setFactory(array(new Definition('Bar\FooClass'), 'getInstance')) + ->register('service_from_anonymous_factory', '%foo%') + ->setFactory(array(new Definition('%foo%'), 'getInstance')) ->setPublic(true) ; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php index 667dd852a0..1092363fd3 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services19.php @@ -21,6 +21,8 @@ class ProjectServiceContainer extends Container public function __construct() { + $this->parameters = $this->getDefaultParameters(); + $this->services = array(); $this->methodMap = array( 'service_from_anonymous_factory' => 'getServiceFromAnonymousFactoryService', @@ -58,11 +60,11 @@ class ProjectServiceContainer extends Container /** * Gets the public 'service_from_anonymous_factory' shared service. * - * @return \Bar\FooClass + * @return object A %env(FOO)% instance */ protected function getServiceFromAnonymousFactoryService() { - return $this->services['service_from_anonymous_factory'] = (new \Bar\FooClass())->getInstance(); + return $this->services['service_from_anonymous_factory'] = (new ${($_ = $this->getEnv('FOO')) && false ?: "_"}())->getInstance(); } /** @@ -78,4 +80,102 @@ class ProjectServiceContainer extends Container return $instance; } + + public function getParameter($name) + { + $name = (string) $name; + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = $this->normalizeParameterName($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + public function hasParameter($name) + { + $name = (string) $name; + $name = $this->normalizeParameterName($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array( + 'foo' => false, + ); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + switch ($name) { + case 'foo': $value = $this->getEnv('FOO'); break; + default: throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + $this->loadedDynamicParameters[$name] = true; + + return $this->dynamicParameters[$name] = $value; + } + + private $normalizedParameterNames = array( + 'env(foo)' => 'env(FOO)', + ); + + private function normalizeParameterName($name) + { + if (isset($this->normalizedParameterNames[$normalizedName = strtolower($name)]) || isset($this->parameters[$normalizedName]) || array_key_exists($normalizedName, $this->parameters)) { + $normalizedName = isset($this->normalizedParameterNames[$normalizedName]) ? $this->normalizedParameterNames[$normalizedName] : $normalizedName; + if ((string) $name !== $normalizedName) { + @trigger_error(sprintf('Parameter names will be made case sensitive in Symfony 4.0. Using "%s" instead of "%s" is deprecated since Symfony 3.4.', $name, $normalizedName), E_USER_DEPRECATED); + } + } else { + $normalizedName = $this->normalizedParameterNames[$normalizedName] = (string) $name; + } + + return $normalizedName; + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'env(FOO)' => 'Bar\\FaooClass', + ); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php new file mode 100644 index 0000000000..8e9e65ad98 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_adawson.php @@ -0,0 +1,184 @@ +services = array(); + $this->normalizedIds = array( + 'app\\bus' => 'App\\Bus', + 'app\\db' => 'App\\Db', + 'app\\handler1' => 'App\\Handler1', + 'app\\handler2' => 'App\\Handler2', + 'app\\processor' => 'App\\Processor', + 'app\\registry' => 'App\\Registry', + 'app\\schema' => 'App\\Schema', + ); + $this->methodMap = array( + 'App\\Bus' => 'getBusService', + 'App\\Db' => 'getDbService', + 'App\\Handler1' => 'getHandler1Service', + 'App\\Handler2' => 'getHandler2Service', + 'App\\Processor' => 'getProcessorService', + 'App\\Registry' => 'getRegistryService', + 'App\\Schema' => 'getSchemaService', + ); + $this->privates = array( + 'App\\Handler1' => true, + 'App\\Handler2' => true, + 'App\\Processor' => true, + 'App\\Registry' => true, + 'App\\Schema' => true, + ); + + $this->aliases = array(); + } + + public function getRemovedIds() + { + return array( + 'App\\Handler1' => true, + 'App\\Handler2' => true, + 'App\\Processor' => true, + 'App\\Registry' => true, + 'App\\Schema' => true, + 'Psr\\Container\\ContainerInterface' => true, + 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, + ); + } + + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled() + { + return true; + } + + public function isFrozen() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED); + + return true; + } + + /** + * Gets the public 'App\Bus' shared service. + * + * @return \App\Bus + */ + protected function getBusService() + { + $this->services['App\Bus'] = $instance = new \App\Bus(${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}); + + $instance->handler1 = ${($_ = isset($this->services['App\Handler1']) ? $this->services['App\Handler1'] : $this->getHandler1Service()) && false ?: '_'}; + $instance->handler2 = ${($_ = isset($this->services['App\Handler2']) ? $this->services['App\Handler2'] : $this->getHandler2Service()) && false ?: '_'}; + + return $instance; + } + + /** + * Gets the public 'App\Db' shared service. + * + * @return \App\Db + */ + protected function getDbService() + { + $this->services['App\Db'] = $instance = new \App\Db(); + + $instance->schema = ${($_ = isset($this->services['App\Schema']) ? $this->services['App\Schema'] : $this->getSchemaService()) && false ?: '_'}; + + return $instance; + } + + /** + * Gets the private 'App\Handler1' shared service. + * + * @return \App\Handler1 + */ + protected function getHandler1Service() + { + $a = ${($_ = isset($this->services['App\Processor']) ? $this->services['App\Processor'] : $this->getProcessorService()) && false ?: '_'}; + + if (isset($this->services['App\Handler1'])) { + return $this->services['App\Handler1']; + } + + return $this->services['App\Handler1'] = new \App\Handler1(${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}, ${($_ = isset($this->services['App\Schema']) ? $this->services['App\Schema'] : $this->getSchemaService()) && false ?: '_'}, $a); + } + + /** + * Gets the private 'App\Handler2' shared service. + * + * @return \App\Handler2 + */ + protected function getHandler2Service() + { + return $this->services['App\Handler2'] = new \App\Handler2(${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}, ${($_ = isset($this->services['App\Schema']) ? $this->services['App\Schema'] : $this->getSchemaService()) && false ?: '_'}, ${($_ = isset($this->services['App\Processor']) ? $this->services['App\Processor'] : $this->getProcessorService()) && false ?: '_'}); + } + + /** + * Gets the private 'App\Processor' shared service. + * + * @return \App\Processor + */ + protected function getProcessorService() + { + $a = ${($_ = isset($this->services['App\Registry']) ? $this->services['App\Registry'] : $this->getRegistryService()) && false ?: '_'}; + + if (isset($this->services['App\Processor'])) { + return $this->services['App\Processor']; + } + + return $this->services['App\Processor'] = new \App\Processor($a, ${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}); + } + + /** + * Gets the private 'App\Registry' shared service. + * + * @return \App\Registry + */ + protected function getRegistryService() + { + $this->services['App\Registry'] = $instance = new \App\Registry(); + + $instance->processor = array(0 => ${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}, 1 => ${($_ = isset($this->services['App\Bus']) ? $this->services['App\Bus'] : $this->getBusService()) && false ?: '_'}); + + return $instance; + } + + /** + * Gets the private 'App\Schema' shared service. + * + * @return \App\Schema + */ + protected function getSchemaService() + { + $a = ${($_ = isset($this->services['App\Db']) ? $this->services['App\Db'] : $this->getDbService()) && false ?: '_'}; + + if (isset($this->services['App\Schema'])) { + return $this->services['App\Schema']; + } + + return $this->services['App\Schema'] = new \App\Schema($a); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_self_ref.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_self_ref.php new file mode 100644 index 0000000000..5cb1002fea --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_self_ref.php @@ -0,0 +1,78 @@ +services = array(); + $this->normalizedIds = array( + 'app\\foo' => 'App\\Foo', + ); + $this->methodMap = array( + 'App\\Foo' => 'getFooService', + ); + + $this->aliases = array(); + } + + public function getRemovedIds() + { + return array( + 'Psr\\Container\\ContainerInterface' => true, + 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, + ); + } + + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled() + { + return true; + } + + public function isFrozen() + { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED); + + return true; + } + + /** + * Gets the public 'App\Foo' shared service. + * + * @return \App\Foo + */ + protected function getFooService() + { + $b = new \App\Bar(); + $a = new \App\Baz($b); + + $this->services['App\Foo'] = $instance = new \App\Foo($a); + + $b->foo = $instance; + + $a->bar = $b; + + return $instance; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_adawson.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_adawson.yml new file mode 100644 index 0000000000..2a26f38a83 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_adawson.yml @@ -0,0 +1,28 @@ +services: + App\Db: + public: true + properties: + schema: '@App\Schema' + + App\Bus: + public: true + arguments: ['@App\Db'] + properties: + handler1: '@App\Handler1' + handler2: '@App\Handler2' + + App\Handler1: + ['@App\Db', '@App\Schema', '@App\Processor'] + + App\Handler2: + ['@App\Db', '@App\Schema', '@App\Processor'] + + App\Processor: + ['@App\Registry', '@App\Db'] + + App\Registry: + properties: + processor: ['@App\Db', '@App\Bus'] + + App\Schema: + arguments: ['@App\Db']