-
-
Notifications
You must be signed in to change notification settings - Fork 327
Compile factories, including closures #507
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
d8c56b0 to
6ec16df
Compare
Factory definitions are now dumped into the compiled container! This follows #494 where all definitions but factories, decorators and wildcards were compiled. Decorators and wildcards are still not compiled. Compiling factories means also compiling closures. That was actually much more doable than I thought, I've used [Roave/BetterReflection](https://github.com/Roave/BetterReflection) (thanks asgrim and ocramius!) The main downside of the whole pull request is the number of dependencies that BetterReflection brings: - Installing symfony/polyfill-util (v1.4.0) Loading from cache - Installing symfony/polyfill-php56 (v1.4.0) Loading from cache - Installing nikic/php-parser (v2.1.1) Loading from cache - Installing jeremeamia/superclosure (2.3.0) Loading from cache - Installing zendframework/zend-eventmanager (3.1.0) Loading from cache - Installing zendframework/zend-code (3.1.0) Loading from cache - Installing phpdocumentor/reflection-common (1.0) Loading from cache - Installing phpdocumentor/type-resolver (0.2.1) Loading from cache - Installing phpdocumentor/reflection-docblock (2.0.5) Loading from cache - Installing roave/better-reflection (1.2.0) Loading from cache Maybe we could get on with superclosure only. Or else make compiling closures optional (only compile them if BetterReflection is installed…). Also I've seen that there is a v2.0 of BetterReflection in development [but it requires 7.1](https://github.com/Roave/BetterReflection#changes-in-br-20), I have no idea for how long the v1 will be supported (e.g. will it support PHP 7.2). PHP-DI could upgrade to 7.1 at the end of this year but that's not for sure yet. Performance improvements: -20% on the factory.php benchmark, [profile](https://blackfire.io/profiles/compare/6692ca3e-521a-49df-96c6-1bf9833c8209/graph), which is very good for a micro-benchmark IMO. I'll test all that in larger scenarios for the complete 6.0 release but it's safe to say this PR is an improvement. There are also a lot of micro/not-so-micro optimisations possible here in the future, especially because there are a lot of parameter-guessing done at runtime (you can inject dependencies into the factories and PHP-DI figures it out with type-hints). https://github.com/PHP-DI/Invoker could be compiled in the future (optionally), and simpler scenarios (classic closures) could be much more optimized than that. Work for later! :) Because of closures, some scenarios are explicitly not supported: - you should not use `$this` inside closures - you should not import variables inside the closure using the `use` keyword, like in `function () use ($foo) { ...` - you should not define multiple closures on the same line But those scenarios don't make sense in container factories written in array config so that's good!
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.
Good work! Unsure about the dependencies - it is indeed a large dependency graph, but I also don't have a good solution to fix that, sorry :-\
composer.json
Outdated
| }, | ||
| "config": { | ||
| "platform": { | ||
| "php": "7.0" |
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.
uhmmmmmmmm
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.
What was I trying to do 🤔
| protected function resolveFactory($callable, $entryName, array $extraParameters = []) | ||
| { | ||
| // Initialize the factory resolver | ||
| if (! $this->factoryInvoker) { |
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.
Make this a private getter
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 wanted to avoid an unnecessary method call here, see the profile of the difference: https://blackfire.io/profiles/compare/5a49dac2-a8f2-454d-9074-598540040477/graph?settings%5Bdimension%5D=wt&settings%5Bdisplay%5D=focused&settings%5BtabPane%5D=nodes&selected=&callname=DI%5CCompiledContainer%3A%3AgetFactoryInvoker
Not much but if that can be avoided.
| break; | ||
| case $definition instanceof FactoryDefinition: | ||
| $value = $definition->getCallable(); | ||
|
|
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.
Method should not be 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.
Variable name does not reflect its meaning: this is not a name, it's a bool
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'm not sure I should check for the staticness, PHP already throws a warning at compile time and it still works so I don't think PHP-DI should choose to make this stricter.
src/Compiler.php
Outdated
| $value = $definition->getCallable(); | ||
|
|
||
| // Custom error message to help debugging | ||
| $isInvokableClassName = is_string($value) && class_exists($value) && method_exists($value, '__invoke'); |
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.
You should really start using named constructors for exceptions :-P
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 do for common ones but this error message is very specific to that line so moving it elsewhere would just spread logic around (cohesion ftw)
src/Compiler.php
Outdated
| if ($value instanceof \Closure) { | ||
| try { | ||
| $reflection = ReflectionFunction::createFromClosure($value); | ||
| } catch (TwoClosuresOneLine $e) { |
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 you really want to catch this? Also, make sure to re-throw the previous exception if you do.
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 want to hide the implementation details of the closure compiler, but good catch on forwarding the previous exception.
|
|
||
| return "[\n$value ]"; | ||
| } | ||
|
|
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.
Move to private method
src/Compiler.php
Outdated
| $ast = $reflection->getAst(); | ||
|
|
||
| // Force all closures to be static (add the `static` keyword), i.e. they can't use | ||
| // $this, which makes sense since their code is copied into another class. |
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.
You are using the API from PHP-Parser here, so you should add that to your composer.json too
src/ContainerBuilder.php
Outdated
|
|
||
| if ($this->compileToFile) { | ||
| $containerClass = (new Compiler)->compile($source, $this->compileToFile); | ||
| $autowiringEnabled = $this->useAutowiring || $this->useAnnotations; |
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.
Variable is redundant
You'll get security support, no worries :-) |
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.
You'll get security support, no worries :-)
Nice to know!
Thanks for the review!
composer.json
Outdated
| }, | ||
| "config": { | ||
| "platform": { | ||
| "php": "7.0" |
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.
What was I trying to do 🤔
| protected function resolveFactory($callable, $entryName, array $extraParameters = []) | ||
| { | ||
| // Initialize the factory resolver | ||
| if (! $this->factoryInvoker) { |
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 wanted to avoid an unnecessary method call here, see the profile of the difference: https://blackfire.io/profiles/compare/5a49dac2-a8f2-454d-9074-598540040477/graph?settings%5Bdimension%5D=wt&settings%5Bdisplay%5D=focused&settings%5BtabPane%5D=nodes&selected=&callname=DI%5CCompiledContainer%3A%3AgetFactoryInvoker
Not much but if that can be avoided.
src/Compiler.php
Outdated
| if ($value instanceof \Closure) { | ||
| try { | ||
| $reflection = ReflectionFunction::createFromClosure($value); | ||
| } catch (TwoClosuresOneLine $e) { |
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 want to hide the implementation details of the closure compiler, but good catch on forwarding the previous exception.
| break; | ||
| case $definition instanceof FactoryDefinition: | ||
| $value = $definition->getCallable(); | ||
|
|
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'm not sure I should check for the staticness, PHP already throws a warning at compile time and it still works so I don't think PHP-DI should choose to make this stricter.
src/Compiler.php
Outdated
| $value = $definition->getCallable(); | ||
|
|
||
| // Custom error message to help debugging | ||
| $isInvokableClassName = is_string($value) && class_exists($value) && method_exists($value, '__invoke'); |
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 do for common ones but this error message is very specific to that line so moving it elsewhere would just spread logic around (cohesion ftw)
# Conflicts: # src/Compiler.php # src/ContainerBuilder.php # tests/IntegrationTest/CompiledContainerTest.php
|
Less Composer dependencies are now installed:
|
|
@mnapoli would it be a good idea to have the factory compiling split out and handled as a dev dependency by consumer projects? Just wondering/rambling, not necessarily feasible or relevant. |
|
That could be a good idea indeed (considering the whole compilation step to make sense only in dev env, not prod) but I'd like to keep the whole thing simple (simply calling a method on the |
Factory definitions are now dumped into the compiled container!
This follows #494 where all definitions but factories, decorators and wildcards were compiled. Decorators and wildcards are still not compiled.
Compiling factories means also compiling closures. That was actually much more doable than I thought, I've used Roave/BetterReflection (thanks @asgrim and @Ocramius!)
The main downside of the whole pull request is the number of dependencies that BetterReflection brings:
Maybe we could get on with superclosure only. Or else make compiling closures optional (only compile them if BetterReflection is installed…). Also I've seen that there is a v2.0 of BetterReflection in development but it requires 7.1, I have no idea for how long the v1 will be supported (e.g. will it support PHP 7.2). PHP-DI could upgrade to 7.1 at the end of this year but that's not for sure yet.
Performance improvements: -20% on the factory.php benchmark, profile, which is very good for a micro-benchmark IMO. I'll test all that in larger scenarios for the complete 6.0 release but it's safe to say this PR is an improvement.
There are also a lot of micro/not-so-micro optimisations possible here in the future, especially because there are a lot of parameter-guessing done at runtime (you can inject dependencies into the factories and PHP-DI figures it out with type-hints). https://github.com/PHP-DI/Invoker could be compiled in the future (optionally), and simpler scenarios (classic closures) could be much more optimized than that. Work for later! :)
Because of closures, some scenarios are explicitly not supported:
$thisinside closuresusekeyword, like infunction () use ($foo) { ...But those scenarios don't make sense in container factories written in array config so that's good!