-
Notifications
You must be signed in to change notification settings - Fork 25
Implement factory creation #52
Implement factory creation #52
Conversation
Creates and returns the string contents for the factory class based on reflecting the given class. Factories: - use strict mode - are generic classes defining `__invoke`, which accepts a PSR-11 container - import the PSR-11 container and any classes the class constructor depends on, even if they are in the same namespace - pull class dependencies from the container
Composes a `FactoryClassGenerator` in order to produce a factory for the given class name, and then writes that class in a sibling file. If the class is not autoloadable, `Create` raises an exception. If unable to write the file to the filesystem, `Create` also raises an exception. The factory method, `createForClass`, returns the filename where the factory class was written.
|
maybe add config option for this? or ask for the first time or even more fun it could check if exists Factory directory then create there instead |
|
This hugely complicates the command, and makes errors during generation far more likely. The only way it would work reliably is if, in addition to the class name for which to generate the factory, we required both the class name for the new factory, and the path where the factory should be created. At that point, you're typing as much, if not more, than you would if you were to generate the factory and then perform a |
`ConfigInjector` creates and/or updates a special autoloadable config file, zend-expressive-tooling-factories.global.php. Its `injectFactoryForClass()` method expects the factory class name and the class name to which it maps. It slurps up the existing configuration, if any, and then adds the new mapping, sorts the entries, and writes the file back to its location.
Modifies the CreateFactoryCommand to register the new factory for the class using the ConfigInjector by default. Users can opt to disable this feature via the `--no-register` option.
When `middleware:create` is called, it now generates a factory for the generated middleware, as a sibling class, using `factory:create`. Users can disable this by passing the `--no-factory` class. Users can also generate the factory, but not register it, by passing the `--no-register` option.
Updates the `handler:create` command to trigger the `factory:create` command with the name of the generated handler. Users may disable generation of the factory using the option `--no-factory`. Users may generate the factory, but omit registration of the handler/factory pair by passing the option `--no-register`.
When testing the `middleware:create` and `handler:create` commands, I received exceptions during factory generation saying that the middleware and/or handler class could not be found. Adding a `require $path` statement addresses the issue, but introduces new problems during testing, which I addressed by adding a private flag that, when disabled, skips the `require` statement.
*/ | ||
class ConfigInjector | ||
{ | ||
public const CONFIG_FILE = 'config/autoload/zend-expressive-tooling-factories.global.php'; |
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.
Is there a reason why a new file is used and not config/autoload/dependencies.global.php
?
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.
Yes: to prevent issues when re-generating the file:
- serialization of closures
- resolution of constants
- expressions
- usage of import statements
It's far more complex to write to a file meant to be managed manually than to use a file that we can mark as under control of the tooling.
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.
Makes sense. Thanx for the explanation.
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.
It looks like that we are also missing definition of const HELP_ARG_MODULE
in CreateHandlerCommand
and CreateFactoryCommand
.
For reference please see other command and class final class CommandCommonOptions
(line 32)
update: oh, we are not calling addDefaultOptionsAndArguments
so we don't need to define that constant there.
@@ -63,6 +89,22 @@ protected function execute(InputInterface $input, OutputInterface $output) | |||
$path | |||
)); | |||
|
|||
if (! $input->getOption('no-factory')) { | |||
$this->requireHandlerBeforeGeneratingFactory && require $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.
This is unclear, not easy readable. I know how it works, but it looks like 'js optimized code'.
What's more I don't like to include the class that way. Can't we use autoloader? If not, maybe we should use BetterReflection to read the class by reflection without autoloading the class?
If there is no other way than require please change it for more readable if
statement
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.
It's a testing problem, plain and simple. require
works fine in actual use, but in testing, we get an error due to the fact that no resolvable class file is created.
I can update this to be an if
statement; it just feels like overkill for what will be the general use case.
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.
And no, the autoloader does not work in this situation. I'm not entirely sure why. But I tried a number of approaches, and the only way I could get it to be able to reflect the generated class was to require it first. I am reluctant to add BetterReflection
when we can accomplish what we need without the extra dependency.
@@ -63,6 +88,22 @@ protected function execute(InputInterface $input, OutputInterface $output) | |||
$path | |||
)); | |||
|
|||
if (! $input->getOption('no-factory')) { | |||
$this->requireMiddlewareBeforeGeneratingFactory && require $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.
The same as noted above.
src/Factory/ConfigInjector.php
Outdated
throw ConfigFileNotWritableException::forFile($this->configFile); | ||
} | ||
|
||
$config = file_exists($this->configFile) ? include($this->configFile) : []; |
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 usually we are not using parenthesis with include
. We have this check in upcoming zend-coding-standards
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 had done this because originally I was calculating the filename inline; I'll remove the parens in an upcoming commit.
src/Factory/ConfigInjector.php
Outdated
|
||
private function configIsWritable() : bool | ||
{ | ||
if (! file_exists($this->configFile) && ! is_writable(dirname($this->configFile))) { |
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 can simplify this function, according to the php docs:
Returns TRUE if the filename exists and is writable.
http://php.net/is_writable
So it's not enough just return is_writable($this->configFile)
?
(it checks existence)
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.
Noted; thanks!
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.
Actually, this only applies to the following conditional, not the one flagged. Note that this one is testing explicitly that the parent directory is writable when the file does not exist.
The one following we can rewrite to ! is_writable($this->configFile)
, as we're then testing that it both exists and is writable.
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.
Okay, interestingly: tests FAIL when I change this! I'm not sure if this is a difference with how vfsStream handles the is_writable()
call, or if the documented behavior of is_writable
may be incorrect. Regardless, it needs to stay as-is.
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, I can check it later, for now it's fine for me. 👍
|
||
use RuntimeException; | ||
|
||
class FactoryAlreadyExistsException extends RuntimeException |
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 extract all Exceptions to namespace, as we have in different project.
I can see we don't have it here in commands, but we can refactor it later on as well.
Here we have more exceptions so it will be clearer to have them in separate directory/namespace (each for command ofc)
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'd rather not. Most of these commands are:
- A command class
- One or more utility classes to which they delegate
- One or more exceptions
The exceptions are specific to the specific command domain, and having a separate namespace is overkill, even when there are multiple different types. Consdiering the majority have a single exception type, forcing each to have a subnamespace is imposing hierarchy just for the sake of it.
$constructorParameters, | ||
function (ReflectionParameter $argument) { | ||
if ($argument->isOptional()) { | ||
return false; |
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 I understand correctly - optional params are not going to be initialized in the factory?
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.
Correct. This is also how zend-servicemanager handles optional arguments in its ReflectionBasedAbstractFactory
and related factory generator.
|
||
return $application; | ||
|
||
$this->command->setApplication($application->reveal()); |
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.
unreachable statement
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.
Oh! That's not good! I'll remove the return
statement!
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.
Actually, need to remove the setApplication()
statements, as they happen in the test cases that call this method.
@@ -43,6 +54,32 @@ private function reflectExecuteMethod() | |||
return $r; | |||
} | |||
|
|||
private function mockApplication() |
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.
As we are using PHP 7.1 I would suggest to add return type (Application
?)
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.
Sure; do we need to add RTH within a test case, though?
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.
We don't need to, but it would be nice, especially for IDEs and writing new test.
I would add docblock with Application|ObjectProphecy
as it is more accurate, and maybe we should go this way, not RTH.
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.
This is... confusing. We can't specify a RTH of Application
, as we actually return a prophecy. But specifying ObjectProphecy
as the RTH makes it less clear what we're returning. I'll document it via a docblock annotation instead.
|
||
return $application; | ||
|
||
$this->command->setApplication($application->reveal()); |
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.
unreachable statement
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.
Will fix; thanks!
$className = InvokableObject::class; | ||
$factory = file_get_contents(__DIR__ . '/TestAsset/factories/InvokableObject.php'); | ||
|
||
self::assertEquals($factory, $this->generator->createFactory($className)); |
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.
Just for consistency, everywhere in this PR you are using $this->
... and maybe we can make a final decision what we are gonna do: self::
or $this->
(or 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.
I'll fix that; not sure why I used self::
here; likely copy-pasta from the original in zend-servicemanager.
- Adds a docblock with the RTH specified as `ObjectProphecy|Application` - Removes `$this->command->setApplication()` calls within the `mockApplication()` methods; these were unreachable, and unwanted.
- Do not test if a `ReflectionClass` was produced; an exception is thrown if one is not. - Use `! $constructorParameters` instead of `empty($constructorParameters)` - An exception class was not imported, due to bad copy-pasta; created a new one with a named constructor, and used that instead.
return substr($className, strrpos($className, '\\') + 1); | ||
} | ||
|
||
private function getConstructorParameters(string $className) : array |
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.
Missing @throws
annotation with the function (UnidentifiedTypeException
)
src/Factory/ConfigInjector.php
Outdated
/** | ||
* Sorts entries by key, using natcase | ||
*/ | ||
private function sort(array $config) : array |
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.
This is basically ksort
with SORT_NATURAL
flag :) So I would suggest to use internal function instead.
…lass Uses `substr` + `strrpos` to calculate it.
… implementation.
ccf16fb
to
1f61691
Compare
This patch
is a work in progress, addressingaddresses #42.factory:create <class>
handler:create
to also generate a factory for the created handler; command should include a--no-factory
switch to disable thismiddleware:create
to also generate a factory for the created middleware; command should include a--no-factory
switch to disable thisconfig/autoload/dependencies.global.php
One question I have: should the command befactory:create
orfactory:create-for
?The final commands are:
expressive factory:create <for class name> [--no-register]
expressive handler:create <class name> [--no-factory] [--no-register]
expressive middleware:create <class name> [--no-factory] [--no-register]