10000 [DependencyInjection] Add `#[AutowireInline]` attribute to allow service definition at the class level by DaDeather · Pull Request #52820 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DependencyInjection] Add #[AutowireInline] attribute to allow service definition at the class level #52820

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 1 commit
Commits
File filter

Filter by extension

Filter by extension 10000

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[DependencyInjection] Add #[AutowireInline] attribute to allow serv…
…ice definition at the class level
  • Loading branch information
DaDeather authored and nicolas-grekas committed May 2, 2024
commit a5961429aaae23f7761fbf242699c6de17398e53
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* Attribute to tell which callable to give to an argument of type Closure.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireCallable extends Autowire
class AutowireCallable extends AutowireInline
{
/**
* @param string|array|null $callable The callable to autowire
Expand All @@ -40,7 +40,7 @@ public function __construct(
throw new LogicException('#[AutowireCallable] attribute cannot have a $method without a $service.');
}

parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
Autowire::__construct($callable ?? [new Reference($service), $method ?? '__invoke'], lazy: $lazy);
}

public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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\Attribute;

use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

/**
* Allows inline service definition for a constructor argument.
* Using this attribute on a class autowires it as a new instance
* which is not shared between different services.
*
* @author Ismail Özgün Turan <oezguen.turan@dadadev.com>
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class AutowireInline extends Autowire
Copy link
Member
@chalasr chalasr Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutowireInline looks pretty cryptic, any newcomer wouldn't be able to get what the attribute is for by just looking at its name nor its description as it currently is. I think we either need to find a super self-explanatory name (I've no clue) or extend the description so that it tells what's the purpose of the attribute and when it should be used (inline service definition only means something to advanced Symfony's DIC hackers)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming things... :)
"inline" refers to "inline_service()" in the PHP-DSL

AutowireInlineService? but verbose
AutowireNew? AutowireObject? AutowireInstance? or keep AutowireInline?

of course, a top notch description is also desirable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a better description here feel free to request further suggestments to it 👍.

{
public function __construct(string|array $class, array $arguments = [], array $calls = [], array $properties = [], ?string $parent = null, bool|string $lazy = false)
{
parent::__construct([
\is_array($class) ? 'factory' : 'class' => $class,
'arguments' => $arguments,
'calls' => $calls,
'properties' => $properties,
'parent' => $parent,
], lazy: $lazy);
}

public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition
{
static $parseDefinition;
static $yamlLoader;

$parseDefinition ??= new \ReflectionMethod(YamlFileLoader::class, 'parseDefinition');
$yamlLoader ??= $parseDefinition->getDeclaringClass()->newInstanceWithoutConstructor();

if (isset($value['factory'])) {
$value['class'] = $type;
$value['factory'][0] ??= $type;
$value['factory'][1] ??= '__invoke';
}
$class = $parameter->getDeclaringClass();

return $parseDefinition->invoke($yamlLoader, $class->name, $value, $class->getFileName(), ['autowire' => true], true);
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CHANGELOG
* Add argument `$prepend` to `FileLoader::construct()` to prepend loaded configuration instead of appending it
* [BC BREAK] When used in the `prependExtension()` method, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it
* Cast env vars to null or bool when referencing them using `#[Autowire(env: '...')]` depending on the signature of the corresponding parameter
* Add `#[AutowireInline]` attribute to allow service definition at the class level

7.0
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
use Symfony\Component\DependencyInjection\Attribute\Lazy;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -331,9 +331,9 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
continue 2;
}

if ($attribute instanceof AutowireCallable) {
if ($attribute instanceof AutowireInline) {
$value = $attribute->buildDefinition($value, $type, $parameter);
$value = $this->doProcessValue($value);
$value = new Reference('.autowire_inline.'.ContainerBuilder::hash($value));
} elseif ($lazy = $attribute->lazy) {
$definition = (new Definition($type))
->setFactory('current')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function __construct()
new AutoAliasServicePass(),
new ValidateEnvPlaceholdersPass(),
new ResolveDecoratorStackPass(),
new ResolveAutowireInlineAttributesPass(),
new ResolveChildDefinitionsPass(),
new RegisterServiceSubscribersPass(),
new ResolveParameterPlaceHoldersPass(false, false),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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\Attribute\AutowireInline;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\VarExporter\ProxyHelper;

/**
* Inspects existing autowired services for {@see AutowireInline} attribute and registers the definitions for reuse.
*
* @author Ismail Özgün Turan <oezguen.turan@dadadev.com>
*/
class ResolveAutowireInlineAttributesPass extends AbstractRecursivePass
{
protected bool $skipScalars = true;

protected function processValue(mixed $value, bool $isRoot = false): mixed
{
$value = parent::processValue($value, $isRoot);

if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
return $value;
}

try {
$constructor = $this->getConstructor($value, false);
} catch (RuntimeException) {
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" cannot be loaded.', $this->currentId, $value->getClass()));

return $value;
}

if ($constructor === null) {
return $value;
}

$reflectionParameters = $constructor->getParameters();
foreach ($reflectionParameters as $reflectionParameter) {
$autowireInlineAttributes = $reflectionParameter->getAttributes(AutowireInline::class, \ReflectionAttribute::IS_INSTANCEOF);
foreach ($autowireInlineAttributes as $autowireInlineAttribute) {
/** @var AutowireInline $autowireInlineAttributeInstance */
$autowireInlineAttributeInstance = $autowireInlineAttribute->newInstance();

$type = ProxyHelper::exportType($reflectionParameter, true);
$definition = $autowireInlineAttributeInstance->buildDefinition($autowireInlineAttributeInstance->value, $type, $reflectionParameter);

$this->container->setDefinition('.autowire_inline.'.ContainerBuilder::hash($definition), $definition);
}
}

return $value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?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\Tests\Attribute;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Attribute\AutowireInline;
use Symfony\Component\DependencyInjection\Reference;

class AutowireInlineTest extends TestCase
{
public function testInvalidFactoryArray()
{
$autowireInline = new AutowireInline([123, 456]);

self::assertSame([123, 456], $autowireInline->value['factory']);
}

/**
* @dataProvider provideInvalidCalls
*/
public function testInvalidCallsArray(array $calls)
{
$autowireInline = new AutowireInline('someClass', calls: $calls);

self::assertSame('someClass', $autowireInline->value['class']);
self::assertSame($calls, $autowireInline->value['calls']);
}

public static function provideInvalidCalls(): iterable
{
yield 'missing method' => [[[]]];
yield 'invalid method value type1' => [[[null]]];
yield 'invalid method value type2' => [[[123]]];
yield 'invalid method value type3' => [[[true]]];
yield 'invalid method value type4' => [[[false]]];
yield 'invalid method value type5' => [[[new \stdClass()]]];
yield 'invalid method value type6' => [[[[]]]];

yield 'invalid arguments value type1' => [[['someMethod', null]]];
yield 'invalid arguments value type2' => [[['someMethod', 123]]];
yield 'invalid arguments value type3' => [[['someMethod', true]]];
yield 'invalid arguments value type4' => [[['someMethod', false]]];
yield 'invalid arguments value type5' => [[['someMethod', new \stdClass()]]];
yield 'invalid arguments value type6' => [[['someMethod', '']]];
}

public function testClass()
{
$attribute = new AutowireInline('someClass');

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertSame('someClass', $buildDefinition->getClass());
self::assertSame([], $buildDefinition->getArguments());
self::assertFalse($attribute->lazy);
}

public function testClassAndParams()
{
$attribute = new AutowireInline('someClass', ['someParam']);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertSame('someClass', $buildDefinition->getClass());
self::assertSame(['someParam'], $buildDefinition->getArguments());
self::assertFalse($attribute->lazy);
}

public function testClassAndParamsLazy()
{
$attribute = new AutowireInline('someClass', ['someParam'], lazy: true);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertSame('someClass', $buildDefinition->getClass());
self::assertSame(['someParam'], $buildDefinition->getArguments());
self::assertTrue($attribute->lazy);
}

/**
* @dataProvider provideFactories
*/
public function testFactory(string|array $factory, string|array $expectedResult)
{
$attribute = new AutowireInline($factory);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertNull($buildDefinition->getClass());
self::assertEquals($expectedResult, $buildDefinition->getFactory());
self::assertSame([], $buildDefinition->getArguments());
self::assertFalse($attribute->lazy);
}

/**
* @dataProvider provideFactories
*/
public function testFactoryAndParams(string|array $factory, string|array $expectedResult)
{
$attribute = new AutowireInline($factory, ['someParam']);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertNull($buildDefinition->getClass());
self::assertEquals($expectedResult, $buildDefinition->getFactory());
self::assertSame(['someParam'], $buildDefinition->getArguments());
self::assertFalse($attribute->lazy);
}

/**
* @dataProvider provideFactories
*/
public function testFactoryAndParamsLazy(string|array $factory, string|array $expectedResult)
{
$attribute = new AutowireInline($factory, ['someParam'], lazy: true);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertNull($buildDefinition->getClass());
self::assertEquals($expectedResult, $buildDefinition->getFactory());
self::assertSame(['someParam'], $buildDefinition->getArguments());
self::assertTrue($attribute->lazy);
}

public static function provideFactories(): iterable
{
yield 'string callable' => [[null, 'someFunction'], [null, 'someFunction']];

yield 'class only' => [['someClass'], ['someClass', '__invoke']];
yield 'reference only' => [[new Reference('someClass')], [new Reference('someClass'), '__invoke']];

yield 'class with method' => [['someClass', 'someStaticMethod'], ['someClass', 'someStaticMethod']];
yield 'reference with method' => [[new Reference('someClass'), 'someMethod'], [new Reference('someClass'), 'someMethod']];
yield '@reference with method' => [['@someClass', 'someMethod'], [new Reference('someClass'), 'someMethod']];
}

/**
* @dataProvider provideCalls
*/
public function testCalls(string|array $calls, array $expectedResult)
{
$attribute = new AutowireInline('someClass', calls: $calls);

$buildDefinition = $attribute->buildDefinition($attribute->value, null, $this->createReflectionParameter());

self::assertSame('someClass', $buildDefinition->getClass());
self::assertSame($expectedResult, $buildDefinition->getMethodCalls());
self::assertSame([], $buildDefinition->getArguments());
self::assertFalse($attribute->lazy);
}

public static function provideCalls(): iterable
{
yield 'method with empty arguments' => [
[['someMethod', []]],
[['someMethod', []]],
];
yield 'method with arguments' => [
[['someMethod', ['someArgument']]],
[['someMethod', ['someArgument']]],
];
yield 'method without arguments with return clone true' => [
[['someMethod', [], true]],
[['someMethod', [], true]],
];
yield 'method without arguments with return clone false' => [
[['someMethod', [], false]],
[['someMethod', []]],
];
yield 'method with arguments with return clone true' => [
[['someMethod', ['someArgument'], true]],
[['someMethod', ['someArgument'], true]],
];
yield 'method with arguments with return clone false' => [
[['someMethod', ['someArgument'], false]],
[['someMethod', ['someArgument']]],
];
}

private function createReflectionParameter()
{
$class = new class('someValue') {
public function __construct($someParameter)
{
}
};
$reflectionClass = new \ReflectionClass($class);

return $reflectionClass->getConstructor()->getParameters()[0];
}
}
Loading
0