feature #37545 [DependencyInjection] Add the Required attribute (derrabus)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[DependencyInjection] Add the Required attribute

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | N/A
| License       | MIT
| Doc PR        | TODO

This PR proposes a new attribute `#[Required]` that can be used instead of the `@required` annotation.

Commits
-------

ea262441e7 [DependencyInjection] Add the Required attribute.
This commit is contained in:
Fabien Potencier 2020-09-07 14:06:46 +02:00
commit b26f11ca0b
16 changed files with 175 additions and 11 deletions

View File

@ -22,6 +22,7 @@ foreach ($loader->getClassMap() as $class => $file) {
case false !== strpos($file, '/src/Symfony/Component/Debug/Tests/Fixtures/'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes_80.php'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/uniontype_classes.php'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ParentNotExists.php'):
case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'):

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Contracts\Service\Attribute\Required;
/**
* Looks for definitions with autowiring enabled and registers their corresponding "@required" methods as setters.
@ -49,6 +50,14 @@ class AutowireRequiredMethodsPass extends AbstractRecursivePass
}
while (true) {
if (\PHP_VERSION_ID >= 80000 && $r->getAttributes(Required::class)) {
if ($this->isWither($r, $r->getDocComment() ?: '')) {
$withers[] = [$r->name, [], true];
} else {
$value->addMethodCall($r->name, []);
}
break;
}
if (false !== $doc = $r->getDocComment()) {
if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
if ($this->isWither($reflectionMethod, $doc)) {

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\Attribute\Required;
/**
* Looks for definitions with autowiring enabled and registers their corresponding "@required" properties.
@ -45,10 +46,9 @@ class AutowireRequiredPropertiesPass extends AbstractRecursivePass
if (!($type = $reflectionProperty->getType()) instanceof \ReflectionNamedType) {
continue;
}
if (false === $doc = $reflectionProperty->getDocComment()) {
continue;
}
if (false === stripos($doc, '@required') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
if ((\PHP_VERSION_ID < 80000 || !$reflectionProperty->getAttributes(Required::class))
&& ((false === $doc = $reflectionProperty->getDocComment()) || false === stripos($doc, '@required') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc))
) {
continue;
}
if (\array_key_exists($name = $reflectionProperty->getName(), $properties)) {

View File

@ -28,6 +28,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\Attribute\Required;
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
@ -640,6 +641,32 @@ class AutowirePassTest extends TestCase
);
}
/**
* @requires PHP 8
*/
public function testSetterInjectionWithAttribute()
{
if (!class_exists(Required::class)) {
$this->markTestSkipped('symfony/service-contracts 2.2 required');
}
$container = new ContainerBuilder();
$container->register(Foo::class);
$container
->register('setter_injection', AutowireSetter::class)
->setAutowired(true);
(new ResolveClassPass())->process($container);
(new AutowireRequiredMethodsPass())->process($container);
(new AutowirePass())->process($container);
$methodCalls = $container->getDefinition('setter_injection')->getMethodCalls();
$this->assertCount(1, $methodCalls);
$this->assertSame('setFoo', $methodCalls[0][0]);
$this->assertSame(Foo::class, (string) $methodCalls[0][1][0]);
}
public function testWithNonExistingSetterAndAutowiring()
{
$this->expectException(RuntimeException::class);

View File

@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Tests\Fixtures\WitherStaticReturnType;
use Symfony\Contracts\Service\Attribute\Required;
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
@ -54,6 +55,29 @@ class AutowireRequiredMethodsPassTest extends TestCase
$this->assertEquals([], $methodCalls[1][1]);
}
/**
* @requires PHP 8
*/
public function testSetterInjectionWithAttribute()
{
if (!class_exists(Required::class)) {
$this->markTestSkipped('symfony/service-contracts 2.2 required');
}
$container = new ContainerBuilder();
$container->register(Foo::class);
$container
->register('setter_injection', AutowireSetter::class)
->setAutowired(true);
(new ResolveClassPass())->process($container);
(new AutowireRequiredMethodsPass())->process($container);
$methodCalls = $container->getDefinition('setter_injection')->getMethodCalls();
$this->assertSame([['setFoo', []]], $methodCalls);
}
public function testExplicitMethodInjection()
{
$container = new ContainerBuilder();
@ -124,4 +148,26 @@ class AutowireRequiredMethodsPassTest extends TestCase
];
$this->assertSame($expected, $methodCalls);
}
/**
* @requires PHP 8
*/
public function testWitherInjectionWithAttribute()
{
if (!class_exists(Required::class)) {
$this->markTestSkipped('symfony/service-contracts 2.2 required');
}
$container = new ContainerBuilder();
$container->register(Foo::class);
$container
->register('wither', AutowireWither::class)
->setAutowired(true);
(new ResolveClassPass())->process($container);
(new AutowireRequiredMethodsPass())->process($container);
$this->assertSame([['withFoo', [], true]], $container->getDefinition('wither')->getMethodCalls());
}
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredPropertiesPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Contracts\Service\Attribute\Required;
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
@ -43,4 +44,28 @@ class AutowireRequiredPropertiesPassTest extends TestCase
$this->assertArrayHasKey('plop', $properties);
$this->assertEquals(Bar::class, (string) $properties['plop']);
}
/**
* @requires PHP 8
*/
public function testAttribute()
{
if (!class_exists(Required::class)) {
$this->markTestSkipped('symfony/service-contracts 2.2 required');
}
$container = new ContainerBuilder();
$container->register(Foo::class);
$container->register('property_injection', AutowireProperty::class)
->setAutowired(true);
(new ResolveClassPass())->process($container);
(new AutowireRequiredPropertiesPass())->process($container);
$properties = $container->getDefinition('property_injection')->getProperties();
$this->assertArrayHasKey('foo', $properties);
$this->assertEquals(Foo::class, (string) $properties['foo']);
}
}

View File

@ -6,6 +6,7 @@ use Psr\Log\LoggerInterface;
if (PHP_VERSION_ID >= 80000) {
require __DIR__.'/uniontype_classes.php';
require __DIR__.'/autowiring_classes_80.php';
}
class Foo

View File

@ -0,0 +1,28 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use Symfony\Contracts\Service\Attribute\Required;
class AutowireSetter
{
#[Required]
public function setFoo(Foo $foo): void
{
}
}
class AutowireWither
{
#[Required]
public function withFoo(Foo $foo): static
{
return $this;
}
}
class AutowireProperty
{
#[Required]
public Foo $foo;
}

View File

@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -25,7 +25,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -0,0 +1,27 @@
<?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\Contracts\Service\Attribute;
use Attribute;
/**
* A required dependency.
*
* This attribute indicates that a property holds a required dependency. The annotated property or method should be
* considered during the instantiation process of the containing class.
*
* @author Alexander M. Turek <me@derrabus.de>
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
final class Required
{
}

View File

@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@ -49,7 +49,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "2.2-dev"
}
}
}