diff --git a/src/Symfony/Component/DependencyInjection/Alias.php b/src/Symfony/Component/DependencyInjection/Alias.php index 24484de167e87..e61084c6f0b59 100644 --- a/src/Symfony/Component/DependencyInjection/Alias.php +++ b/src/Symfony/Component/DependencyInjection/Alias.php @@ -11,17 +11,24 @@ namespace Symfony\Component\DependencyInjection; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + class Alias { private $id; private $public; private $private; + private $deprecated; + private $deprecationTemplate; + + private static $defaultDeprecationTemplate = 'The "%service_id%" service alias is deprecated. You should stop using it, as it will soon be removed.'; public function __construct(string $id, bool $public = true) { $this->id = $id; $this->public = $public; $this->private = 2 > \func_num_args(); + $this->deprecated = false; } /** @@ -78,6 +85,46 @@ public function isPrivate() return $this->private; } + /** + * Whether this alias is deprecated, that means it should not be referenced + * anymore. + * + * @param bool $status Whether this alias is deprecated, defaults to true + * @param string $template Optional template message to use if the alias is deprecated + * + * @return $this + * + * @throws InvalidArgumentException when the message template is invalid + */ + public function setDeprecated($status = true, $template = null) + { + if (null !== $template) { + if (preg_match('#[\r\n]|\*/#', $template)) { + throw new InvalidArgumentException('Invalid characters found in deprecation template.'); + } + + if (false === strpos($template, '%service_id%')) { + throw new InvalidArgumentException('The deprecation template must contain the "%service_id%" placeholder.'); + } + + $this->deprecationTemplate = $template; + } + + $this->deprecated = (bool) $status; + + return $this; + } + + public function isDeprecated(): bool + { + return $this->deprecated; + } + + public function getDeprecationMessage(string $id): string + { + return str_replace('%service_id%', $id, $this->deprecationTemplate ?: self::$defaultDeprecationTemplate); + } + /** * Returns the Id of this alias. * diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php index 7777243da96cc..3742d662486da 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.php @@ -31,6 +31,7 @@ public function process(ContainerBuilder $container) foreach ($container->getAliases() as $id => $alias) { $aliasId = (string) $alias; + if ($aliasId !== $defId = $this->getDefinitionId($aliasId, $container)) { $container->setAlias($id, $defId)->setPublic($alias->isPublic())->setPrivate($alias->isPrivate()); } @@ -60,8 +61,15 @@ private function getDefinitionId(string $id, ContainerBuilder $container): strin if (isset($seen[$id])) { throw new ServiceCircularReferenceException($id, array_merge(array_keys($seen), [$id])); } + $seen[$id] = true; - $id = (string) $container->getAlias($id); + $alias = $container->getAlias($id); + + if ($alias->isDeprecated()) { + @trigger_error($alias->getDeprecationMessage($id), E_USER_DEPRECATED); + } + + $id = (string) $alias; } return $id; diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 58a4bfc48422f..25cef99de875a 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -579,7 +579,13 @@ private function doGet($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_ } if (!isset($this->definitions[$id]) && isset($this->aliasDefinitions[$id])) { - return $this->doGet((string) $this->aliasDefinitions[$id], $invalidBehavior, $inlineServices, $isConstructorArgument); + $alias = $this->aliasDefinitions[$id]; + + if ($alias->isDeprecated()) { + @trigger_error($alias->getDeprecationMessage($id), E_USER_DEPRECATED); + } + + return $this->doGet((string) $alias, $invalidBehavior, $inlineServices, $isConstructorArgument); } try { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index b1b3ef9f0080c..473b34b261a4c 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -218,6 +218,7 @@ public function dump(array $options = []) $code = $this->startClass($options['class'], $baseClass, $baseClassWithNamespace). $this->addServices($services). + $this->addDeprecatedAliases(). $this->addDefaultParametersMethod() ; @@ -1115,6 +1116,15 @@ private function addMethodMap(): string } } + $aliases = $this->container->getAliases(); + foreach ($aliases as $alias => $id) { + if (!$id->isDeprecated()) { + continue; + } + $id = (string) $id; + $code .= ' '.$this->doExport($alias).' => '.$this->doExport($this->generateMethodName($alias)).",\n"; + } + return $code ? " \$this->methodMap = [\n{$code} ];\n" : ''; } @@ -1141,6 +1151,10 @@ private function addAliases(): string $code = " \$this->aliases = [\n"; ksort($aliases); foreach ($aliases as $alias => $id) { + if ($id->isDeprecated()) { + continue; + } + $id = (string) $id; while (isset($aliases[$id])) { $id = (string) $aliases[$id]; @@ -1151,6 +1165,39 @@ private function addAliases(): string return $code." ];\n"; } + private function addDeprecatedAliases(): string + { + $code = ''; + $aliases = $this->container->getAliases(); + foreach ($aliases as $alias => $definition) { + if (!$definition->isDeprecated()) { + continue; + } + $public = $definition->isPublic() ? 'public' : 'private'; + $id = (string) $definition; + $methodNameAlias = $this->generateMethodName($alias); + $idExported = $this->export($id); + $messageExported = $this->export($definition->getDeprecationMessage($alias)); + $code = <<docStar} + * Gets the $public '$alias' alias. + * + * @return object The "$id" service. + */ + protected function {$methodNameAlias}() + { + @trigger_error($messageExported, E_USER_DEPRECATED); + + return \$this->get($idExported); + } + +EOF; + } + + return $code; + } + private function addInlineRequires(): string { if (!$this->hotPathTag || !$this->inlineRequires) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index c5292ecf688bf..23e52d0e71d62 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -220,6 +220,10 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults) $alias->setPublic($defaults['public']); } + if ($deprecated = $this->getChildren($service, 'deprecated')) { + $alias->setDeprecated(true, $deprecated[0]->nodeValue ?: null); + } + return; } @@ -668,7 +672,10 @@ private function validateAlias(\DOMElement $alias, $file) } foreach ($alias->childNodes as $child) { - if ($child instanceof \DOMElement && self::NS === $child->namespaceURI) { + if (!$child instanceof \DOMElement && self::NS !== $child->namespaceURI) { + continue; + } + if (!\in_array($child->localName, ['deprecated'], true)) { throw new InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $alias->getAttribute('id'), $file)); } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 05fbe86363a93..f8ebfd7b6b11d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -349,9 +349,13 @@ private function parseDefinition($id, $service, $file, array $defaults) } foreach ($service as $key => $value) { - if (!\in_array($key, ['alias', 'public'])) { + if (!\in_array($key, ['alias', 'public', 'deprecated'])) { throw new InvalidArgumentException(sprintf('The configuration key "%s" is unsupported for the service "%s" which is defined as an alias in "%s". Allowed configuration keys for service aliases are "alias" and "public".', $key, $id, $file)); } + + if ('deprecated' === $key) { + $alias->setDeprecated(true, $value); + } } return; diff --git a/src/Symfony/Component/DependencyInjection/Tests/AliasTest.php b/src/Symfony/Component/DependencyInjection/Tests/AliasTest.php new file mode 100644 index 0000000000000..44144b240b51c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/AliasTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Alias; + +class AliasTest extends TestCase +{ + public function testConstructor() + { + $alias = new Alias('foo'); + + $this->assertEquals('foo', (string) $alias); + $this->assertTrue($alias->isPublic()); + } + + public function testCanConstructANonPublicAlias() + { + $alias = new Alias('foo', false); + + $this->assertEquals('foo', (string) $alias); + $this->assertFalse($alias->isPublic()); + } + + public function testCanConstructAPrivateAlias() + { + $alias = new Alias('foo', false, false); + + $this->assertEquals('foo', (string) $alias); + $this->assertFalse($alias->isPublic()); + $this->assertFalse($alias->isPrivate()); + } + + public function testCanSetPublic() + { + $alias = new Alias('foo', false); + $alias->setPublic(true); + + $this->assertTrue($alias->isPublic()); + } + + public function testCanDeprecateAnAlias() + { + $alias = new Alias('foo', false); + $alias->setDeprecated(true, 'The %service_id% service is deprecated.'); + + $this->assertTrue($alias->isDeprecated()); + } + + public function testItHasADefaultDeprecationMessage() + { + $alias = new Alias('foo', false); + $alias->setDeprecated(); + + $expectedMessage = 'The "foo" service alias is deprecated. You should stop using it, as it will soon be removed.'; + $this->assertEquals($expectedMessage, $alias->getDeprecationMessage('foo')); + } + + public function testReturnsCorrectDeprecationMessage() + { + $alias = new Alias('foo', false); + $alias->setDeprecated(true, 'The "%service_id%" is deprecated.'); + + $expectedMessage = 'The "foo" is deprecated.'; + $this->assertEquals($expectedMessage, $alias->getDeprecationMessage('foo')); + } + + public function testCanOverrideDeprecation() + { + $alias = new Alias('foo', false); + $alias->setDeprecated(); + + $initial = $alias->isDeprecated(); + $alias->setDeprecated(false); + $final = $alias->isDeprecated(); + + $this->assertTrue($initial); + $this->assertFalse($final); + } + + /** + * @dataProvider invalidDeprecationMessageProvider + * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException + */ + public function testCannotDeprecateWithAnInvalidTemplate($message) + { + $def = new Alias('foo'); + $def->setDeprecated(true, $message); + } + + public function invalidDeprecationMessageProvider() + { + return [ + "With \rs" => ["invalid \r message %service_id%"], + "With \ns" => ["invalid \n message %service_id%"], + 'With */s' => ['invalid */ message %service_id%'], + 'message not containing required %service_id% variable' => ['this is deprecated'], + ]; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php index 55b47057b1fd5..96a59639a5a26 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveReferencesToAliasesPassTest.php @@ -83,6 +83,48 @@ public function testResolveFactory() $this->assertSame('Factory', (string) $resolvedBarFactory[0]); } + /** + * @group legacy + * @expectedDeprecation The "deprecated_foo_alias" service alias is deprecated. You should stop using it, as it will soon be removed. + */ + public function testDeprecationNoticeWhenReferencedByAlias() + { + $container = new ContainerBuilder(); + + $container->register('foo', 'stdClass'); + + $aliasDeprecated = new Alias('foo'); + $aliasDeprecated->setDeprecated(true); + $container->setAlias('deprecated_foo_alias', $aliasDeprecated); + + $alias = new Alias('deprecated_foo_alias'); + $container->setAlias('alias', $alias); + + $this->process($container); + } + + /** + * @group legacy + * @expectedDeprecation The "foo_aliased" service alias is deprecated. You should stop using it, as it will soon be removed. + */ + public function testDeprecationNoticeWhenReferencedByDefinition() + { + $container = new ContainerBuilder(); + + $container->register('foo', 'stdClass'); + + $aliasDeprecated = new Alias('foo'); + $aliasDeprecated->setDeprecated(true); + $container->setAlias('foo_aliased', $aliasDeprecated); + + $container + ->register('definition') + ->setArguments([new Reference('foo_aliased')]) + ; + + $this->process($container); + } + protected function process(ContainerBuilder $container) { $pass = new ResolveReferencesToAliasesPass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 83c0458e5c671..cec7ea4d183de 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -259,6 +259,22 @@ public function testAliases() } } + /** + * @group legacy + * @expectedDeprecation The "foobar" service alias is deprecated. You should stop using it, as it will soon be removed. + */ + public function testDeprecatedAlias() + { + $builder = new ContainerBuilder(); + $builder->register('foo', 'stdClass'); + + $alias = new Alias('foo'); + $alias->setDeprecated(); + $builder->setAlias('foobar', $alias); + + $builder->get('foobar'); + } + public function testGetAliases() { $builder = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 6f1fb76719cce..214c619a1bbea 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -325,6 +325,24 @@ public function testAliases() $this->assertSame($foo, $container->get('alias_for_alias')); } + /** + * @group legacy + * @expectedDeprecation The "alias_for_foo_deprecated" service alias is deprecated. You should stop using it, as it will soon be removed. + */ + public function testAliasesDeprecation() + { + $container = include self::$fixturesPath.'/containers/container_alias_deprecation.php'; + $container->compile(); + $dumper = new PhpDumper($container); + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/container_alias_deprecation.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Aliases_Deprecation'])); + + require self::$fixturesPath.'/php/container_alias_deprecation.php'; + $container = new \Symfony_DI_PhpDumper_Test_Aliases_Deprecation(); + $container->get('alias_for_foo_non_deprecated'); + $container->get('alias_for_foo_deprecated'); + } + public function testFrozenContainerWithoutAliases() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_alias_deprecation.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_alias_deprecation.php new file mode 100644 index 0000000000000..b9369172a2b43 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_alias_deprecation.php @@ -0,0 +1,21 @@ +register('foo', 'stdClass') + ->setPublic(true) +; + +$container + ->setAlias('alias_for_foo_deprecated', 'foo') + ->setDeprecated(true) + ->setPublic(true); + +$container + ->setAlias('alias_for_foo_non_deprecated', 'foo') + ->setPublic(true); + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_alias_deprecation.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_alias_deprecation.php new file mode 100644 index 0000000000000..c7f6c5432ffea --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/container_alias_deprecation.php @@ -0,0 +1,73 @@ +services = $this->privates = []; + $this->methodMap = [ + 'foo' => 'getFooService', + 'alias_for_foo_deprecated' => 'getAliasForFooDeprecatedService', + ]; + $this->aliases = [ + 'alias_for_foo_non_deprecated' => 'foo', + ]; + } + + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled() + { + return true; + } + + public function getRemovedIds() + { + return [ + 'Psr\\Container\\ContainerInterface' => true, + 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, + ]; + } + + /** + * Gets the public 'foo' shared service. + * + * @return \stdClass + */ + protected function getFooService() + { + return $this->services['foo'] = new \stdClass(); + } + + /** + * Gets the public 'alias_for_foo_deprecated' alias. + * + * @return object The "foo" service. + */ + protected function getAliasForFooDeprecatedService() + { + @trigger_error('The "alias_for_foo_deprecated" service alias is deprecated. You should stop using it, as it will soon be removed.', E_USER_DEPRECATED); + + return $this->get('foo'); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/deprecated_alias_definitions.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/deprecated_alias_definitions.xml new file mode 100644 index 0000000000000..83ceeefa9c163 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/deprecated_alias_definitions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + The "%service_id%" service alias is deprecated. + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_alias_definitions.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_alias_definitions.yml new file mode 100644 index 0000000000000..2223597815f6d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_alias_definitions.yml @@ -0,0 +1,4 @@ +services: + alias_for_foobar: + alias: foobar + deprecated: The "%service_id%" service alias is deprecated. diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 6d9634c66cba8..e5037d809b593 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -351,6 +351,21 @@ public function testDeprecated() $this->assertSame($message, $container->getDefinition('bar')->getDeprecationMessage('bar')); } + public function testDeprecatedAliases() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('deprecated_alias_definitions.xml'); + + $this->assertTrue($container->getAlias('alias_for_foo')->isDeprecated()); + $message = 'The "alias_for_foo" service alias is deprecated. You should stop using it, as it will soon be removed.'; + $this->assertSame($message, $container->getAlias('alias_for_foo')->getDeprecationMessage('alias_for_foo')); + + $this->assertTrue($container->getAlias('alias_for_foobar')->isDeprecated()); + $message = 'The "alias_for_foobar" service alias is deprecated.'; + $this->assertSame($message, $container->getAlias('alias_for_foobar')->getDeprecationMessage('alias_for_foobar')); + } + public function testConvertDomElementToArray() { $doc = new \DOMDocument('1.0'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 37e6054f5545f..8c9ccaf06ff93 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -175,6 +175,17 @@ public function testLoadServices() $this->assertEquals(['decorated', 'decorated.pif-pouf', 5], $services['decorator_service_with_name_and_priority']->getDecoratedService()); } + public function testDeprecatedAliases() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('deprecated_alias_definitions.yml'); + + $this->assertTrue($container->getAlias('alias_for_foobar')->isDeprecated()); + $message = 'The "alias_for_foobar" service alias is deprecated.'; + $this->assertSame($message, $container->getAlias('alias_for_foobar')->getDeprecationMessage('alias_for_foobar')); + } + public function testLoadFactoryShortSyntax() { $container = new ContainerBuilder();