Skip to content

Commit 3e80e46

Browse files
committed
[DependencyInjection] Add a mechanism to deprecate public services to private
1 parent e9be741 commit 3e80e46

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class UnusedTagsPass implements CompilerPassInterface
3434
'container.hot_path',
3535
'container.no_preload',
3636
'container.preload',
37+
'container.private',
3738
'container.reversible',
3839
'container.service_locator',
3940
'container.service_locator_context',

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ CHANGELOG
2121
* deprecated `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead
2222
* deprecated PHP-DSL's `inline()` function, use `service()` instead
2323
* added support of PHP8 static return type for withers
24+
* added `AliasDeprecatedPublicServicesPass` to deprecate public services to private
2425

2526
5.0.0
2627
-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
final class AliasDeprecatedPublicServicesPass extends AbstractRecursivePass
19+
{
20+
private $tagName;
21+
22+
private $aliases = [];
23+
24+
public function __construct(string $tagName = 'container.private')
25+
{
26+
$this->tagName = $tagName;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
protected function processValue($value, bool $isRoot = false)
33+
{
34+
if ($value instanceof Reference && isset($this->aliases[$id = (string) $value])) {
35+
return new Reference($this->aliases[$id], $value->getInvalidBehavior());
36+
}
37+
38+
return parent::processValue($value, $isRoot);
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function process(ContainerBuilder $container)
45+
{
46+
foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
47+
if (null === $package = $tags[0]['package'] ?? null) {
48+
throw new InvalidArgumentException(sprintf('The "package" attribute is mandatory for the "%s" tag on the "%s" service.', $this->tagName, $id));
49+
}
50+
51+
if (null === $version = $tags[0]['version'] ?? null) {
52+
throw new InvalidArgumentException(sprintf('The "version" attribute is mandatory for the "%s" tag on the "%s" service.', $this->tagName, $id));
53+
}
54+
55+
$definition = $container->getDefinition($id);
56+
if (!$definition->isPublic() || $definition->isPrivate()) {
57+
throw new InvalidArgumentException(sprintf('The "%s" service is private: it cannot have the "%s" tag.', $id, $this->tagName));
58+
}
59+
60+
$container
61+
->setAlias($id, $aliasId = '.'.$this->tagName.'.'.$id)
62+
->setPublic(true)
63+
->setDeprecated($package, $version, 'Accessing the "%alias_id%" service directly from the container is deprecated, use dependency injection instead.');
64+
65+
$container->setDefinition($aliasId, $definition);
66+
67+
$this->aliases[$id] = $aliasId;
68+
}
69+
70+
parent::process($container);
71+
}
72+
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public function __construct()
9494
new CheckExceptionOnInvalidReferenceBehaviorPass(),
9595
new ResolveHotPathPass(),
9696
new ResolveNoPreloadPass(),
97+
new AliasDeprecatedPublicServicesPass(),
9798
]];
9899
}
99100

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\DependencyInjection\Compiler\AliasDeprecatedPublicServicesPass;
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
9+
10+
final class AliasDeprecatedPublicServicesPassTest extends TestCase
11+
{
12+
public function testProcess()
13+
{
14+
$container = new ContainerBuilder();
15+
$container
16+
->register('foo')
17+
->setPublic(true)
18+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.2']);
19+
20+
(new AliasDeprecatedPublicServicesPass())->process($container);
21+
22+
$this->assertTrue($container->hasAlias('foo'));
23+
24+
$alias = $container->getAlias('foo');
25+
26+
$this->assertSame('.container.private.foo', (string) $alias);
27+
$this->assertTrue($alias->isPublic());
28+
$this->assertFalse($alias->isPrivate());
29+
$this->assertSame([
30+
'package' => 'foo/bar',
31+
'version' => '1.2',
32+
'message' => 'Accessing the "foo" service directly from the container is deprecated, use dependency injection instead.',
33+
], $alias->getDeprecation('foo'));
34+
}
35+
36+
/**
37+
* @dataProvider processWithMissingAttributeProvider
38+
*/
39+
public function testProcessWithMissingAttribute(string $attribute, array $attributes)
40+
{
41+
$this->expectException(InvalidArgumentException::class);
42+
$this->expectExceptionMessage(sprintf('The "%s" attribute is mandatory for the "container.private" tag on the "foo" service.', $attribute));
43+
44+
$container = new ContainerBuilder();
45+
$container
46+
->register('foo')
47+
->addTag('container.private', $attributes);
48+
49+
(new AliasDeprecatedPublicServicesPass())->process($container);
50+
}
51+
52+
public function processWithMissingAttributeProvider()
53+
{
54+
return [
55+
['package', ['version' => '1.2']],
56+
['version', ['package' => 'foo/bar']],
57+
];
58+
}
59+
60+
public function testProcessWithNonPublicService()
61+
{
62+
$this->expectException(InvalidArgumentException::class);
63+
$this->expectExceptionMessage('The "foo" service is private: it cannot have the "container.private" tag.');
64+
65+
$container = new ContainerBuilder();
66+
$container
67+
->register('foo')
68+
->setPublic(false)
69+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.2']);
70+
71+
(new AliasDeprecatedPublicServicesPass())->process($container);
72+
}
73+
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

+38
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,44 @@ public function testAutoAliasing()
16611661

16621662
$this->assertInstanceOf(D::class, $container->get(X::class));
16631663
}
1664+
1665+
/**
1666+
* @group legacy
1667+
*/
1668+
public function testDirectlyAccessingDeprecatedPublicService()
1669+
{
1670+
$this->expectDeprecation('Since foo/bar 3.8: Accessing the "Symfony\Component\DependencyInjection\Tests\A" service directly from the container is deprecated, use dependency injection instead.');
1671+
1672+
$container = new ContainerBuilder();
1673+
$container
1674+
->register(A::class)
1675+
->setPublic(true)
1676+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
1677+
1678+
$container->compile();
1679+
1680+
$container->get(A::class);
1681+
}
1682+
1683+
public function testReferencingDeprecatedPublicService()
1684+
{
1685+
$container = new ContainerBuilder();
1686+
$container
1687+
->register(A::class)
1688+
->setPublic(true)
1689+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
1690+
$container
1691+
->register(B::class)
1692+
->setPublic(true)
1693+
->addArgument(new Reference(A::class));
1694+
1695+
$container->compile();
1696+
1697+
// No deprecation should be triggered.
1698+
$container->get(B::class);
1699+
1700+
$this->addToAssertionCount(1);
1701+
}
16641702
}
16651703

16661704
class FooClass

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

+48
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,54 @@ public function testDumpServiceWithAbstractArgument()
14291429
$dumper = new PhpDumper($container);
14301430
$dumper->dump();
14311431
}
1432+
1433+
/**
1434+
* @group legacy
1435+
*/
1436+
public function testDirectlyAccessingDeprecatedPublicService()
1437+
{
1438+
$this->expectDeprecation('Since foo/bar 3.8: Accessing the "bar" service directly from the container is deprecated, use dependency injection instead.');
1439+
1440+
$container = new ContainerBuilder();
1441+
$container
1442+
->register('bar', \BarClass::class)
1443+
->setPublic(true)
1444+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
1445+
1446+
$container->compile();
1447+
1448+
$dumper = new PhpDumper($container);
1449+
eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Directly_Accessing_Deprecated_Public_Service']));
1450+
1451+
$container = new \Symfony_DI_PhpDumper_Test_Directly_Accessing_Deprecated_Public_Service();
1452+
1453+
$container->get('bar');
1454+
}
1455+
1456+
public function testReferencingDeprecatedPublicService()
1457+
{
1458+
$container = new ContainerBuilder();
1459+
$container
1460+
->register('bar', \BarClass::class)
1461+
->setPublic(true)
1462+
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
1463+
$container
1464+
->register('bar_user', \BarUserClass::class)
1465+
->setPublic(true)
1466+
->addArgument(new Reference('bar'));
1467+
1468+
$container->compile();
1469+
1470+
$dumper = new PhpDumper($container);
1471+
eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Referencing_Deprecated_Public_Service']));
1472+
1473+
$container = new \Symfony_DI_PhpDumper_Test_Referencing_Deprecated_Public_Service();
1474+
1475+
// No deprecation should be triggered.
1476+
$container->get('bar_user');
1477+
1478+
$this->addToAssertionCount(1);
1479+
}
14321480
}
14331481

14341482
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

0 commit comments

Comments
 (0)