Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[DependencyInjection] Autoconfigurable attributes on methods, properties and parameters#42039

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
fabpot merged 1 commit intosymfony:5.4fromruudk:method-attributes
Aug 30, 2021

Conversation

@ruudk
Copy link
Contributor

@ruudkruudk commentedJul 9, 2021
edited
Loading

QA
Branch?5.4
Bug fix?no
New feature?yes
Deprecations?no
Tickets
LicenseMIT
Doc PR

Introduction

#39897 introduced the possibility auto configure classes that were annotated with attributes:

$container->registerAttributeForAutoconfiguration(    MyAttribute::class,staticfunction (ChildDefinition$definition,MyAttribute$attribute,\ReflectionClass$reflector):void {$definition->addTag('my_tag', ['some_property' =>$attribute->someProperty]);    });

This works great. But it only works when the attribute is added on the class.

With this PR, it's now possible to also auto configure methods, properties and parameters.

How does it work?

Let's say you have an attribute that targets classes and methods like this:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]finalclass MyAttribute{}

You have two services that use them:

#[MyAttribute]class MyService {}class MyOtherService {    #[MyAttribute]publicfunctionmyMethod() {}}

You can now useregisterAttributeForAutoconfiguration in your extension, together with a union of the types that you want to seach for. In this example, the extension only cares for classes and methods, so it uses\ReflectionClass|\ReflectionMethod $reflector:

finalclass MyBundleExtensionextends Extension{publicfunctionload(array$configs,ContainerBuilder$container) :void    {$container->registerAttributeForAutoconfiguration(            MyAttribute::class,staticfunction (ChildDefinition$definition,MyAttribute$attribute,\ReflectionClass|\ReflectionMethod$reflector) :void {$args = [];if ($reflectorinstanceof \ReflectionMethod) {$args['method'] =$reflector->getName();                }$definition->addTag('my.tag',$args);            }        );    }}

This will tagMyService withmy.tag and it will tagMyOtherService withmy.tag, method: myMethod

If the extension also wants to target the properties that are annotated with attributes, it can either change the union to\ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector or it can just use\Reflector $reflector and do the switching in the callable.

Another example

Let's say you have an attribute like this:

#[Attribute(Attribute::TARGET_CLASS)]finalclass MyAttribute{}

and you use it like this:

$container->registerAttributeForAutoconfiguration(    MyAttribute::class,staticfunction (ChildDefinition$definition,MyAttribute$attribute,\ReflectionClass|\ReflectionMethod$reflector) :void {$definition->addTag('my.tag');    });

you'll get an error saying thatReflectionMethod is not possible as the attribute only targets classes.

nicolas-grekas, jdreesen, hvt, cristoforocervino, and aschempp reacted with hooray emoji
stof
stof previously requested changesJul 9, 2021
@nicolas-grekas
Copy link
Member

Scanning all methods of all autoconfigured services might be a perf hog on real apps.
We should understand the impact this could have before going this way.
Can you run some benchmark and report your findings please?
IIRC, Doctrine does it with an extra annotation on the class, basically saying "have a look at methods".

@nicolas-grekasnicolas-grekas added this to the5.4 milestoneJul 9, 2021
@ruudk
Copy link
ContributorAuthor

@nicolas-grekas Not sure how toproperly benchmark this, but I used aStopwatch in mylarge application and these are the results:
Class attributes took 3.000000 ms for 4469 classes
Method attributes took 18.500000 ms for 4469 classes and total of 41920 methods

Benchmark code
private Stopwatch$stopwatch;private int$classes =0;private int$methods =0;publicfunction__destruct()  {dump(sprintf('Class attributes took %f ms for %d classes',$this->stopwatch->getEvent('class')->getDuration(),$this->classes      ));dump(sprintf('Method attributes took %f ms for %d classes and total of %d methods',$this->stopwatch->getEvent('method')->getDuration(),$this->classes,$this->methods      ));  }protectedfunctionprocessValue($value,bool$isRoot =false)  {// ...$this->stopwatch->start('class');      ++$this->classes;foreach ($reflector->getAttributes()as$attribute) {if ($configurator =$autoconfiguredAttributes[$attribute->getName()] ??null) {$configurator($conditionals,$attribute->newInstance(),$reflector);          }      }$this->stopwatch->stop('class');$this->stopwatch->start('method');foreach ($reflector->getMethods()as$method) {          ++$this->methods;foreach ($method->getAttributes()as$attribute) {if ($configurator =$autoconfiguredAttributes[$attribute->getName()] ??null) {$configurator($conditionals,$attribute->newInstance(),$method);              }          }      }$this->stopwatch->stop('method');// ...  }

Not sure if this is acceptable and how it can be improved.

@nicolas-grekas
Copy link
Member

Thanks for the bench, this looks acceptable!

Copy link
Member

@nicolas-grekasnicolas-grekas left a comment

Choose a reason for hiding this comment

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

I'd suggest extending this to properties and parameters.

In order to play safe and optimize perf a bit, I'd suggest doing some reflection in theprocess() method to classify configurators in 4 groups: the ones that work on classes (ie the ones that have only two arguments or that have a 3rd arg that accept ReflectionClass), the ones that work on methods, properties, and parameters. If a group is empty, we would skip scanning for the corresponding type of attributes.

ruudk reacted with thumbs up emoji
@ruudk
Copy link
ContributorAuthor

ruudk commentedJul 10, 2021
edited
Loading

In order to play safe and optimize perf a bit, I'd suggest doing some reflection in the process() method to classify configurators in 4 groups: the ones that work on classes (ie the ones that have only two arguments or that have a 3rd arg that accept ReflectionClass), the ones that work on methods, properties, and parameters. If a group is empty, we would skip scanning for the corresponding type of attributes.

@nicolas-grekas I applied this feedback and pushed a WIP commit.

Since Attributes only work on PHP 8 I thought it would be nice to support Union types when you want to register an attribute for multiple methods.

So for my example of theAsEventListener I want to typehint that withReflectionClass | ReflectionMethod.

This works, but it will produce a fatal error on PHP 7.4 because it doesn't support unions there. How can we fix that?
Or should we target theAsEventListener change for Symfony 6 as that requires PHP 8 and up?

I'm also not sure howReflectionParameter should be implemented. Should that work on all methods? Or only promoted parameters in the constructor? Aren't those already picked byReflectionProperty?

Todo

  • How to handleAsEventListener on method change? Move to Symfony 6 or find alternative for Union?
  • Add and fix all tests
  • ReflectionParameter

Copy link
Member

@nicolas-grekasnicolas-grekas left a comment

Choose a reason for hiding this comment

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

LGTM, I only have 2 minor comments.
Thanks for completing this quickly!

ruudk reacted with hooray emoji
@ruudk
Copy link
ContributorAuthor

Much better!

@fabpot
Copy link
Member

@ruudk Can you work on a PR for the docs, or at least describe the feature with examples in the PR description? Thank you.

foreach ($reflector->getAttributes()as$attribute) {
if ($configurator =$autoconfiguredAttributes[$attribute->getName()] ??null) {
$configurator($conditionals,$attribute->newInstance(),$reflector);
$conditionals =$instanceof[$classReflector->getName()] ??newChildDefinition('');
Copy link
Contributor

Choose a reason for hiding this comment

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

I would also put all of this new code into a new method

Copy link
ContributorAuthor

@ruudkruudkAug 28, 2021
edited
Loading

Choose a reason for hiding this comment

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

Will it really make it more clear? I don't agree, but if you give me a name for the method I'll do the change.

Copy link
Contributor

@TobionTobion left a comment

Choose a reason for hiding this comment

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

I like it now. It will probably have some performance impact on container building. But I think using attributes for this might be the future. So there is no way around it. This will probably replace the marker interface solutions currently in place when there are such attributes as alternatives.

@ruudk
Copy link
ContributorAuthor

@fabpot I updated the PR description and added a few examples. Hope this helps!

@fabpot
Copy link
Member

@fabpot I updated the PR description and added a few examples. Hope this helps!

That's perfect, thank you.

@fabpot
Copy link
Member

Thank you@ruudk.

ruudk reacted with hooray emoji

@fabpotfabpot merged commita05b6a3 intosymfony:5.4Aug 30, 2021
@ruudkruudk deleted the method-attributes branchAugust 30, 2021 12:57
fabpot added a commit that referenced this pull requestSep 1, 2021
This PR was merged into the 6.0 branch.Discussion----------remove unneeded eval given PHP 8 in 6.0| Q             | A| ------------- | ---| Branch?       | 6.0| Bug fix?      | no| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->| Tickets       || License       | MIT| Doc PR        |Remove eval() added in#42039 for 6.0Commits-------1a3ec8c remove unneeded eval given PHP 8 in 6.0
This was referencedNov 5, 2021
@ruudk
Copy link
ContributorAuthor

ruudk commentedDec 14, 2021
edited
Loading

@nicolas-grekas This PHPStan annotation doesn't work as we expect it to work

* @param callable(ChildDefinition, T, \Reflector): void $configurator

Seephpstan/phpstan#6200

How can we solve this?

$container->registerAttributeForAutoconfiguration(            WithFlow::class,staticfunction (ChildDefinition$definition,WithFlow$attribute,ReflectionMethod$reflector) :void {$definition->addTag(ServiceTag::WITH_FLOW, ['flow' =>$attribute->getFlowName(),                ]);            }        );

gives this error:

phpstan: Parameter #2 $configurator of method Symfony\Component\DependencyInjection\ContainerBuilder::registerAttributeForAutoconfiguration() expects callable(Symfony\Component\DependencyInjection\ChildDefinition, WithoutFlow, Reflector): void, Closure(Symfony\Component\DependencyInjection\ChildDefinition, WithoutFlow, ReflectionMethod): void given.

@nicolas-grekas
Copy link
Member

nicolas-grekas commentedDec 14, 2021
edited
Loading

Closure(Reflector)|Closure(ReflectionMethod)|Closure(ReflectionMethod|ReflectionProperty)|etc

I'm not sure phpstan supports any simple way to express the closures we support here. Ignoring the error might be just fine.

ruudk reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@fabpotfabpotfabpot approved these changes

@nicolas-grekasnicolas-grekasnicolas-grekas approved these changes

@stofstofAwaiting requested review from stof

+1 more reviewer

@TobionTobionTobion approved these changes

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Projects

None yet

Milestone

5.4

Development

Successfully merging this pull request may close these issues.

6 participants

@ruudk@nicolas-grekas@Tobion@fabpot@stof@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp