-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DI] Add prototype services for PSR4-based discovery and registration #21289
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
Conversation
bf7018a
to
dbfac50
Compare
$directory = $this->locator->locate($directory, $this->currentDir, true); | ||
$directory = realpath($directory) ?: $directory; | ||
if (!$classes = $this->findClasses($psr4Glob, $directory)) { | ||
throw new InvalidArgumentException(sprintf('No classes matching "%s" were found in directory "%s".', $psr4Glob, $directory)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we log something instead of throwing? It will allow to register empty directories (useful for starter packs for instance).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log added
|
||
$psr4Prefix = $match[0]; | ||
$classRegexp = self::patternToRegexp($psr4Glob); | ||
$excludeTest = false === strpos($psr4Glob, 'Test', strlen($psr4Prefix)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to hardcode such behaviors? If the user doesn't want to include the Test/
he can already register all subdirectories manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we do, because it gives a good default: test folder/classes usually contain strange things that can break PHP (look at our own test suite - this is were I needed this default first).
} | ||
} | ||
|
||
private static function patternToRegexp($pattern) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the method is private and called only from non-static methods, there is no benefit to make it static.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
d4cc560
to
6584292
Compare
👍 |
Does this also support Symfony specific conventions like parameters and the |
parent::setCurrentDir($dir); | ||
} | ||
|
||
public function registerClasses(Definition $prototype, $psr4Glob, $directory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be protected
, right?
I also suggest to make it final
or @internal
as I don't think it is useful to extend it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be usable by people using the PhpFileLoader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, good point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About final, I don't adhere to the trend of adding it everywhere, so no :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo final methods are great because of the incoming 7.0 type hints: it will allow us to upgrade a bigger part of the codebase.
However, if this RFC is voted, it will be less useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I seriously hope that Parameter Type Widening
is not performed when strict-types are used (in the child class file) 😑 this would be a big mistake otherwise.
@sstok should be yes. |
@@ -23,6 +27,7 @@ | |||
abstract class FileLoader extends BaseFileLoader | |||
{ | |||
protected $container; | |||
protected $currentDir; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private done
6584292
to
4705112
Compare
I voted 😕, so let's explain some of my thoughts about this feature:
|
and we have to account for the success of Laravel because of its DXness. Here, it's plain explicit - and plain local.
You asked for it. No convention here.
that does not work, because what matters is to be able to express a pattern that can be expanded to generate service ids.
same here.
Ask DunglasActionBundle supporters (221 stars, not bad).
I think it's a good default - but if I can't convince the majority, I'll remove it (but I still think that a good default and that not excluding it will prove soon to lead to more problems than WTFs :) )
Sure it does! It's really easy to opt-in "Test" classes. Last word on your other comments about your feelings: I do understand them. |
@nicolas-grekas please note that I'm 😕 and not 👎. I'm just not sure whether I like this or not, but I just wanted to share my current thoughts. |
$classes[] = $class; | ||
continue; | ||
} | ||
if (!function_exists('opcache_is_script_cached') || !opcache_is_script_cached($info->getRealPath() ?: $path)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't something rejected by this check, be also rejected by the class_exists
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having a script in cache does not mean there is a class inside - so that's not the same
@wouterj a few thoughts:
That's not true: a service can easily be added via a Compiler pass, and a few classes a service locators which are not declared as services, e.g. Controllers and (console) Commands. The reason why DunglasApiBundle is popular, is that it allows you to remove those service locators (Controllers & Commands) and make use of dependency injection, without introducing the pain of having to declare them as services (although you do add the necessity to ensure your autowiring bindings are correct).
imports:
- { resource: ../src/Controller, psr4_prefix: AppBundle\Controller\ } I must say that it makes more sense to me as well, with Maybe you would need to add a type: imports:
- { resource: ../src/Controller, type: autowired-services } |
that would be black magic as defined earlier: some global context leaks in!
that is strong coupling with autowiring. This feature is orthogonal: autowiring is not required by this PR at all. The fact that it plays well with autowiring is not an unwanted side effect of course, but things are better decoupled+composed than strongly tightened together. |
So the few points lost in the edit:
A few other things lost but can't remember them... |
the code uses
yes, and for consistency with #21270, I'm going to use a real
same logic as today: the last registered ones wins. Which means you should put these auto-registering declarations before the typical ones and you'll get the behavior you describe.
That's the reason why this needs to be in the "services" section and not in "imports": eg this works: services:
App\Command\:
psr4: ../src/Command
tags: [console.command] this will tag all discovered classes with the console.command tag DunglasActionBundle provides one additional feature that this PR does not try to implement: |
@nicolas-grekas thanks for the answers. Overall, I'm in favour of this mechanism: there is parts on my application where loosening the control whilst still using dependency injection over service locators which I prefer. There is however a big caveat which I'm not happy at all with: side effects.
To extend on the second point, in Laravel you can try to retrieve any class from the DIC. It may not succeed, but it will try at least, even if the class in question is not meant to be registered in the DIC. Now in Symfony we are adopting a similar thing with this mechanism, more scoped as we are specifying the folders we want to apply this mechanism to, but the problem remains the same: you are registering things that should not be registered. To this problem, I think we should, at least, proving the tooling necessary for the people how mind it like me to "uncover" this side-effect. For example a command listing the services registered in this way. In case this seems weird to you I'm giving you the use case I have in mind: I get my hand on a application I'm not familiar with, the glob is hard to understand or I suspect a class is registered when it should not or simply, I want to see if what I expect to be registered actually is, how can I see that easily?
Fair enough in a very simple case, but say mind the tag name needs a bit of processing to be determined (and is currently done in a Compiler pass which does something a la DunglasApiBundle). To extend a bit on this use case: it seems much more interesting to me, more than this PR to be honest, interesting to provide a way to re-use that logic of "find recursively the classes here, return an array of FQC => serviceDefinition", to which you could hook something to do whatever kind of processing you would like to in a compiler pass. Granted this may not be the scope of this PR, but I think it affects the way this feature is implemented (if we want to provide this kind of hooks). |
@nicolas-grekas I'm asking those questions here as I'm missing a bit the technical insight to be able to judge if this requires to be done upfront or if this can be done gradually, in which case this PR is just a first step. If you think this can be done gradually, I'm in favour to open dedicated issue for this PR and just 👍 this one, your call. |
046d5b5
to
5389d98
Compare
That could also apply to "imports". That's why ordering is the only rule to me - for consistency.
that is #18268 - discuss that there - nothing to do with this PR :)
exactly the topic I mentionned before: DunglasActionBundle provides conditional tagging - and this PR does not try to do it, because that's something orthogonal - ie that must be done indenpendently, because it is not required for making this PR self consistent.
same here, that's building on top of this PR, or reusing some of its logic to do something else. Not required to make this PR self consistent. All in all @theofidry, I'm really happy to see that this PR is raising many ideas in your mind. |
5389d98
to
ccda044
Compare
PR updated, trying to listen to comments:
|
eb69028
to
28420c2
Compare
@fabpot comments addressed |
28420c2
to
4999035
Compare
4999035
to
75d66d8
Compare
@@ -659,13 +692,15 @@ private function loadFromExtensions(array $content) | |||
*/ | |||
private static function checkDefinition($id, array $definition, $file) | |||
{ | |||
$keywords = isset($definition['resources']) ? static::$prototypeKeywords : static::$serviceKeywords; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private properties must be accessed using self::
, not static::
. Otherwise, the code breaks when extending the class
return $classes; | ||
} | ||
|
||
private function glob($resources, $recursive, &$prefixLen = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we ever call it in non-recursive mode ? I see a single usage of this method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's preparing for #21270
75d66d8
to
0564cd8
Compare
@@ -0,0 +1,3 @@ | |||
services: | |||
Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\: | |||
resources: ../Prototype |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have named it resource
. A plural seems to indicate that the value is an array, which is not supported here.
0564cd8
to
6b7892c
Compare
Tests are broken. |
6b7892c
to
03470b7
Compare
back to green |
Thank you @nicolas-grekas. |
…nd registration (nicolas-grekas) This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Add prototype services for PSR4-based discovery and registration | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | to be done Talking with @dunglas, we wondered if this could be a good idea, as a more general approach to folder-based service registration as done in [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle/blob/master/DependencyInjection/DunglasActionExtension.php). So here is the implementation. This allows one to define a set of services as such: ```yaml services: App\: resources: ../src/{Controller,Command} # relative to the current file as usual autowire: true # or any other attributes really ``` This looks for php files in the "src" folder, derivates PSR-4 class names from them, and uses `class_exists` for final discovery. The resulting services are named after the classes found this way. The "resource" attribute can be a glob to select only a subset of the classes/files. This approach has several advantages over [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle/blob/master/DependencyInjection/DunglasActionExtension.php): - it is resilient to missing parent classes (see test case) - it loads classes using the normal code path (the autoloader), thus play well with them (e.g. if one registered a special autoloader). - it is more predictable, because it uses discovered paths as the only source of ids/classes to register - vs relying on `get_declared_classes`, which would make things context sensitive. Fits well with current initiatives to me. Commits ------- 03470b7 [DI] Add "psr4" service attribute for PSR4-based discovery and registration
Awesome job! Finally can drop this bundle doing pretty much the same thing: https://github.com/Symplify/AutoServiceRegistration |
…loader (weaverryan) This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Fixing missing "exclude" functionality from PSR4 loader | Q | A | ------------- | --- | Branch? | master | Bug fix? | yes | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | none | License | MIT | Doc PR | TODO When the PSR4 loader was added in #21289, @nicolas-grekas said this: > given that glob() is powerful enough to include/exclude dirs, I removed the Test special exclusion (source: #21289 (comment)) But, I don't believe that's true! [Glob is all about inclusion, not exclusion](https://en.wikipedia.org/wiki/Glob_(programming)#Syntax) - the maximum you can exclude is a single character. Thus, I've marked this as a bug. My motivation came from upgrading KnpU to the new 3.3 DI stuff. We have many directories (40) in `src/`, and listing them all one-by-one in `resource:` is crazy - the `resource` line would be ~350 characters long and quite unreadable. What I really want to do is include *everything* and then exclude few directories. I tried to do this with `glob`, but it's not possible. This PR allows for something like this: ```yml services: _defaults: public: false # ... AppBundle\: resource: '../../src/AppBundle/*' exclude: '../../src/AppBundle/{AppBundle.php,Entity}' ``` This works *beautifully* in practice. And even if I forget to exclude a directory - since the services are private - **they're ultimately removed from the container anyways**. In fact, the *only* reason I need to exclude *anything* is because of the new "service argument resolver", which causes entities to not be properly removed from the compiled container. Thanks! Commits ------- 7d07f19 Allowing prototype/PSR4 service loading ability to exclude, because glob doesn't support this
Talking with @dunglas, we wondered if this could be a good idea, as a more general approach to folder-based service registration as done in DunglasActionBundle.
So here is the implementation.
This allows one to define a set of services as such:
This looks for php files in the "src" folder, derivates PSR-4 class names from them, and uses
class_exists
for final discovery. The resulting services are named after the classes found this way.The "resource" attribute can be a glob to select only a subset of the classes/files.
This approach has several advantages over DunglasActionBundle:
get_declared_classes
, which would make things context sensitive.Fits well with current initiatives to me.