-
-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6ec16df
Compile factories
mnapoli c4f260e
Remove unnecessary platform config in composer.json
mnapoli 1d47321
Require nikic/php-parser because we use it directly
mnapoli 1effa92
Move the compilation of closures to a private method
mnapoli 880994c
Rename variable for better readability
mnapoli a0b90a3
Remove useless variable
mnapoli 5c797a0
Merge branch 'master' into compile-factories
mnapoli 22d232f
Refactor closure compilation to require less Composer dependencies
mnapoli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,9 @@ | |
| use DI\Definition\StringDefinition; | ||
| use DI\Definition\ValueDefinition; | ||
| use InvalidArgumentException; | ||
| use PhpParser\Node\Expr\Closure; | ||
| use SuperClosure\Analyzer\AstAnalyzer; | ||
| use SuperClosure\Exception\ClosureAnalysisException; | ||
|
|
||
| /** | ||
| * Compiles the container into PHP code much more optimized for performances. | ||
|
|
@@ -43,12 +46,17 @@ class Compiler | |
| */ | ||
| private $methods = []; | ||
|
|
||
| /** | ||
| * @var bool | ||
| */ | ||
| private $autowiringEnabled; | ||
|
|
||
| /** | ||
| * Compile the container. | ||
| * | ||
| * @return string The compiled container file name. | ||
| */ | ||
| public function compile(DefinitionSource $definitionSource, string $directory, string $className) : string | ||
| public function compile(DefinitionSource $definitionSource, string $directory, string $className, bool $autowiringEnabled) : string | ||
| { | ||
| $fileName = rtrim($directory, '/') . '/' . $className . '.php'; | ||
|
|
||
|
|
@@ -57,6 +65,8 @@ public function compile(DefinitionSource $definitionSource, string $directory, s | |
| return $fileName; | ||
| } | ||
|
|
||
| $this->autowiringEnabled = $autowiringEnabled; | ||
|
|
||
| // Validate that a valid class name was provided | ||
| $validClassName = preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $className); | ||
| if (!$validClassName) { | ||
|
|
@@ -90,6 +100,8 @@ public function compile(DefinitionSource $definitionSource, string $directory, s | |
| } | ||
|
|
||
| /** | ||
| * @throws DependencyException | ||
| * @throws InvalidDefinition | ||
| * @return string The method name | ||
| */ | ||
| private function compileDefinition(string $entryName, Definition $definition) : string | ||
|
|
@@ -140,6 +152,31 @@ private function compileDefinition(string $entryName, Definition $definition) : | |
| $compiler = new ObjectCreationCompiler($this); | ||
| $code = $compiler->compile($definition); | ||
| $code .= "\n return \$object;"; | ||
| break; | ||
| case $definition instanceof FactoryDefinition: | ||
| $value = $definition->getCallable(); | ||
|
|
||
| // Custom error message to help debugging | ||
| $isInvokableClass = is_string($value) && class_exists($value) && method_exists($value, '__invoke'); | ||
| if ($isInvokableClass && !$this->autowiringEnabled) { | ||
| throw new InvalidDefinition(sprintf( | ||
| 'Entry "%s" cannot be compiled. Invokable classes cannot be automatically resolved if autowiring is disabled on the container, you need to enable autowiring or define the entry manually.', | ||
| $entryName | ||
| )); | ||
| } | ||
|
|
||
| $definitionParameters = ''; | ||
| if (!empty($definition->getParameters())) { | ||
| $definitionParameters = ', ' . $this->compileValue($definition->getParameters()); | ||
| } | ||
|
|
||
| $code = sprintf( | ||
| 'return $this->resolveFactory(%s, %s%s);', | ||
| $this->compileValue($value), | ||
| var_export($entryName, true), | ||
| $definitionParameters | ||
| ); | ||
|
|
||
| break; | ||
| default: | ||
| // This case should not happen (so it cannot be tested) | ||
|
|
@@ -184,6 +221,10 @@ public function compileValue($value) : string | |
| return "[\n$value ]"; | ||
| } | ||
|
|
||
| if ($value instanceof \Closure) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move to private method |
||
| return $this->compileClosure($value); | ||
| } | ||
|
|
||
| return var_export($value, true); | ||
| } | ||
|
|
||
|
|
@@ -212,13 +253,13 @@ private function isCompilable($value) | |
|
|
||
| return 'A decorator definition was found but decorators cannot be compiled'; | ||
| } | ||
| if ($value instanceof FactoryDefinition) { | ||
| return 'A factory definition was found but factories cannot be compiled'; | ||
| } | ||
| // All other definitions are compilable | ||
| if ($value instanceof Definition) { | ||
| return true; | ||
| } | ||
| if ($value instanceof \Closure) { | ||
| return true; | ||
| } | ||
| if (is_object($value)) { | ||
| return 'An object was found but objects cannot be compiled'; | ||
| } | ||
|
|
@@ -228,4 +269,38 @@ private function isCompilable($value) | |
|
|
||
| return true; | ||
| } | ||
|
|
||
| private function compileClosure(\Closure $closure) : string | ||
| { | ||
| $closureAnalyzer = new AstAnalyzer; | ||
|
|
||
| try { | ||
| $closureData = $closureAnalyzer->analyze($closure); | ||
| } catch (ClosureAnalysisException $e) { | ||
| if (stripos($e->getMessage(), 'Two closures were declared on the same line') !== false) { | ||
| throw new InvalidDefinition('Cannot compile closures when two closures are defined on the same line', 0, $e); | ||
| } | ||
|
|
||
| throw $e; | ||
| } | ||
|
|
||
| /** @var Closure $ast */ | ||
| $ast = $closureData['ast']; | ||
|
|
||
| // 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. | ||
| $ast->static = true; | ||
|
|
||
| // Check if the closure imports variables with `use` | ||
| if (! empty($ast->uses)) { | ||
| throw new InvalidDefinition('Cannot compile closures which import variables using the `use` keyword'); | ||
| } | ||
|
|
||
| $code = (new \PhpParser\PrettyPrinter\Standard)->prettyPrint([$ast]); | ||
|
|
||
| // Trim spaces and the last `;` | ||
| $code = trim($code, "\t\n\r;"); | ||
|
|
||
| return $code; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace DI\Compiler; | ||
|
|
||
| use DI\Factory\RequestedEntry; | ||
|
|
||
| /** | ||
| * @author Matthieu Napoli <[email protected]> | ||
| */ | ||
| class RequestedEntryHolder implements RequestedEntry | ||
| { | ||
| /** | ||
| * @var string | ||
| */ | ||
| private $name; | ||
|
|
||
| public function __construct(string $name) | ||
| { | ||
| $this->name = $name; | ||
| } | ||
|
|
||
| public function getName() : string | ||
| { | ||
| return $this->name; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
tests/IntegrationTest/Definitions/FactoryDefinition/config.inc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <?php | ||
|
|
||
| // This is a separated file so that StyleCI doesn't detect it as PHP and doesn't try to reformat it | ||
|
|
||
| return [ | ||
| 'factory' => function () { return 'foo'; }, 'factory2' => function () { return 'bar'; }, | ||
| ]; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.