Skip to content

[DependencyInjection] Add a mechanism to deprecate public services to private #36470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class UnusedTagsPass implements CompilerPassInterface
'container.hot_path',
'container.no_preload',
'container.preload',
'container.private',
'container.reversible',
'container.service_locator',
'container.service_locator_context',
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ CHANGELOG
* deprecated `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead
* deprecated PHP-DSL's `inline()` function, use `service()` instead
* added support of PHP8 static return type for withers
* added `AliasDeprecatedPublicServicesPass` to deprecate public services to private

5.0.0
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;

final class AliasDeprecatedPublicServicesPass extends AbstractRecursivePass
{
private $tagName;

private $aliases = [];

public function __construct(string $tagName = 'container.private')
{
$this->tagName = $tagName;
}

/**
* {@inheritdoc}
*/
protected function processValue($value, bool $isRoot = false)
{
if ($value instanceof Reference && isset($this->aliases[$id = (string) $value])) {
return new Reference($this->aliases[$id], $value->getInvalidBehavior());
}

return parent::processValue($value, $isRoot);
}

/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
if (null === $package = $tags[0]['package'] ?? null) {
throw new InvalidArgumentException(sprintf('The "package" attribute is mandatory for the "%s" tag on the "%s" service.', $this->tagName, $id));
}

if (null === $version = $tags[0]['version'] ?? null) {
throw new InvalidArgumentException(sprintf('The "version" attribute is mandatory for the "%s" tag on the "%s" service.', $this->tagName, $id));
}

$definition = $container->getDefinition($id);
if (!$definition->isPublic() || $definition->isPrivate()) {
throw new InvalidArgumentException(sprintf('The "%s" service is private: it cannot have the "%s" tag.', $id, $this->tagName));
}

$container
->setAlias($id, $aliasId = '.'.$this->tagName.'.'.$id)
->setPublic(true)
->setDeprecated($package, $version, 'Accessing the "%alias_id%" service directly from the container is deprecated, use dependency injection instead.');

$container->setDefinition($aliasId, $definition);

$this->aliases[$id] = $aliasId;
}

parent::process($container);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public function __construct()
new CheckExceptionOnInvalidReferenceBehaviorPass(),
new ResolveHotPathPass(),
new ResolveNoPreloadPass(),
new AliasDeprecatedPublicServicesPass(),
]];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\AliasDeprecatedPublicServicesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

final class AliasDeprecatedPublicServicesPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container
->register('foo')
->setPublic(true)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.2']);

(new AliasDeprecatedPublicServicesPass())->process($container);

$this->assertTrue($container->hasAlias('foo'));

$alias = $container->getAlias('foo');

$this->assertSame('.container.private.foo', (string) $alias);
$this->assertTrue($alias->isPublic());
$this->assertFalse($alias->isPrivate());
$this->assertSame([
'package' => 'foo/bar',
'version' => '1.2',
'message' => 'Accessing the "foo" service directly from the container is deprecated, use dependency injection instead.',
], $alias->getDeprecation('foo'));
}

/**
* @dataProvider processWithMissingAttributeProvider
*/
public function testProcessWithMissingAttribute(string $attribute, array $attributes)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('The "%s" attribute is mandatory for the "container.private" tag on the "foo" service.', $attribute));

$container = new ContainerBuilder();
$container
->register('foo')
->addTag('container.private', $attributes);

(new AliasDeprecatedPublicServicesPass())->process($container);
}

public function processWithMissingAttributeProvider()
{
return [
['package', ['version' => '1.2']],
['version', ['package' => 'foo/bar']],
];
}

public function testProcessWithNonPublicService()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" service is private: it cannot have the "container.private" tag.');

$container = new ContainerBuilder();
$container
->register('foo')
->setPublic(false)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.2']);

(new AliasDeprecatedPublicServicesPass())->process($container);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,44 @@ public function testAutoAliasing()

$this->assertInstanceOf(D::class, $container->get(X::class));
}

/**
* @group legacy
*/
public function testDirectlyAccessingDeprecatedPublicService()
{
$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.');

$container = new ContainerBuilder();
$container
->register(A::class)
->setPublic(true)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);

$container->compile();

$container->get(A::class);
}

public function testReferencingDeprecatedPublicService()
{
$container = new ContainerBuilder();
$container
->register(A::class)
->setPublic(true)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
$container
->register(B::class)
->setPublic(true)
->addArgument(new Reference(A::class));

$container->compile();

// No deprecation should be triggered.
$container->get(B::class);

$this->addToAssertionCount(1);
}
}

class FooClass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,54 @@ public function testDumpServiceWithAbstractArgument()
$dumper = new PhpDumper($container);
$dumper->dump();
}

/**
* @group legacy
*/
public function testDirectlyAccessingDeprecatedPublicService()
{
$this->expectDeprecation('Since foo/bar 3.8: Accessing the "bar" service directly from the container is deprecated, use dependency injection instead.');

$container = new ContainerBuilder();
$container
->register('bar', \BarClass::class)
->setPublic(true)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);

$container->compile();

$dumper = new PhpDumper($container);
eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Directly_Accessing_Deprecated_Public_Service']));

$container = new \Symfony_DI_PhpDumper_Test_Directly_Accessing_Deprecated_Public_Service();

$container->get('bar');
}

public function testReferencingDeprecatedPublicService()
{
$container = new ContainerBuilder();
$container
->register('bar', \BarClass::class)
->setPublic(true)
->addTag('container.private', ['package' => 'foo/bar', 'version' => '3.8']);
$container
->register('bar_user', \BarUserClass::class)
->setPublic(true)
->addArgument(new Reference('bar'));

$container->compile();

$dumper = new PhpDumper($container);
eval('?>'.$dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Referencing_Deprecated_Public_Service']));

$container = new \Symfony_DI_PhpDumper_Test_Referencing_Deprecated_Public_Service();

// No deprecation should be triggered.
$container->get('bar_user');

$this->addToAssertionCount(1);
}
}

class Rot13EnvVarProcessor implements EnvVarProcessorInterface
Expand Down