Skip to content
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

Ahead of time factory creation command #180

Merged
merged 32 commits into from
Apr 16, 2023

Conversation

boesing
Copy link
Member

@boesing boesing commented Feb 24, 2023

Q A
Documentation yes
BC Break yes
New Feature yes
RFC yes

Description

Fixes #171

So just to quickly sum this up:

We do have project configuration, so this is not some fictional use-case.
This configuration contains service_manager configuration where some services (usually only depending on interfaces) are registered via ReflectionBasedAbstractFactory:

return [
    'service_manager' => [
        'factories' => [
            OurService::class => ReflectionBasedAbstractFactory::class,
        ],
    ],
];

Even tho, the ReflectionBasedAbstractFactory is an abstract factory, it also extends FactoryInterface and thus can be used as shown above.

  • One of the "good" things is, that there is no need for a dedicated factory.
  • The "bad" thing is, that the factory consumes runtime to use reflection and to determine the appropriate __construct argument values

I've played a little bit in our project and I ended up writing some code (which I now used to contribute here) which allows us to use ReflectionBasedAbstractFactory in development while providing a CLI command to generate a opcachable factory while putting it on the filesystem during release artifact generation.

Usage would be as follows:

Description:
  Creates factories which replace the runtime overhead for `ReflectionBasedAbstractFactory`.

Usage:
  servicemanager:generate-aot-factories [<localConfigFilename>]

Arguments:
  localConfigFilename        Should be a path targeting a filename which will be created so that the config autoloading will pick it up. Using a `.local.php` suffix should verify that the file is overriding existing configuration. [default: "config/autoload/generated-factories.local.php"]

One needs also to register a directory in the application config:

config/autoload/global.php

return [
    \Laminas\ServiceManager\ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH => 'path/to/config/autoloading/directory',
];

Since the path registered in that configuration key MUST also be added to composer.json autoload section using classmap, having this as a required configuration key to use the CLI command seemed to be a good solution.

{
     "autoload": {"classmap": ["path/to/factory/output"]}
}

So after configuring the application as mentioned above, the CLI command could be executed like:

$ php vendor/bin/laminas servicemanager:generate-aot-factories config/autoload/aot-factories.local.php

This will then scan the projects configuration for all container configurations (so everything which looks like a container configuration actually: has to be an array, containing key factories being an array, containing values with either class-string<ReflectionBasedAbstractFactory> or instance of ReflectionBasedAbstractFactory.

So in the end, we should have a bunch of generated factories located in the path/to/factory/output directory (there is a sub-directory per "container") PLUS the aot-factories.local.php file located in config/autoload.

In the end, running composer dump-autoload should "register" all the factories located in path/to/factory/output and the next vendor/bin/laminas servicemanager:generate-aot-factories run should tell us, that there are no (more) services registered via ReflectionBasedAbstractFactory (as the aot-factories.local.php should've overriden the initial application config).

🎉

@boesing

This comment was marked as outdated.

@boesing boesing force-pushed the feature/ahead-of-time-factory-creator branch from c7f87d6 to bd6a362 Compare February 24, 2023 01:56
@bcremer
Copy link

bcremer commented Mar 13, 2023

I like that proposed feature pretty much 👍

But I prefer to keep generated files in a dedicated directory (./data/generated).
I can archive that with your proposed feature by adding a new PhpFileProvider.

$generatedFactoriesConfig = [
    ServiceManagerConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH => __DIR__ . '/../../data/generated/',
    'factory-override-path' => __DIR__ . '/../data/generated/generated-factories.php'
];

$aggregator = new ConfigAggregator([
    ServiceManagerConfigProvider::class,

    new PhpFileProvider(realpath(__DIR__) . '/autoload/{,*.}{global,local}.php'),

    // Add config for generated factories
    new ArrayProvider($generatedFactoriesConfig),

    // Add generated factories
    new PhpFileProvider($generatedFactoriesConfig['factory-override-path']),
]);

return $aggregator->getMergedConfig();

One feature I'd like to propose is adding localConfigFilename to the configuration so I don't have to provide it in the generate-aot-factories call.

php bin/console.php servicemanager:generate-aot-factories  data/generated/generated-factories.php

How about ServiceManagerConfigProvider::CONFIGURATION_KEY_FACTORY_LOCAL_CONFIG_FILENAME?

WDYT?

@boesing
Copy link
Member Author

boesing commented Mar 13, 2023

@bcremer thats exactly how it is proposed here. You have to configure ConfigProvider:: CONFIGURATION_KEY_FACTORY_TARGET_PATH to target to a path where the files have to be stored.
If that is missing, executing the command will output that the config key is missing.

You have to add it as your composer.json needs that directory in its autoload section.

If we are talking about the path which has to be added by arg1 via CLI - that won't make sense to make it configurable as it could differ from CLI. Not sure if I'd like to have that configurable as well, especially because if it is configurable, who do you expect that file to "add" to your config then? The whole config-merge has to be done via either ConfigAggregator or the moduleloader and frankly, having runtime logic which requires configs and merge them on-top of the already merged configs is nothing I'd like to support.

So imho, its up to the projects to specify a path and - in case the path is not automatically picked-up by configuration merge - make the changes to their ConfigAggregator/modulemanager implementations to ensure the file is properly loaded.
So IMHO, you can do what you provided above while synchronizing that path with your CLI execution as you still will have to provide that path via CLI argument to the command.

@bcremer
Copy link

bcremer commented Mar 27, 2023

I withdraw my idea of having a configuration key to configure the path for the override factory mapping (config/autoload/generated-factories.local.php).

For most users the the proposed default is the better choice as there is already the default PhpFileProvider in place to pick up that file.

I still support the proposed Pull Request from @boesing and would like to see it going forward.

There are projects out there which do want to have "autowiring". Since the service manager is not build with that kind of autowiring compatibility, some developers started to use `ReflectionBasedAbstractFactory` so that they can prevent themselves from writing factories.

Since this project does already provide a CLI command to generate factories, having another CLI command which scans the project configuration for services registered via `ReflectionBasedAbstractFactory` should work.

With this change, both `ReflectionBasedAbstractFactory` and `FactoryCreator`-Command do use the same constructor parameter resolving logic. Due to this, we can safely assume that all services registered with a `ReflectionBasedAbstractFactory` can also be generated during CI.

Signed-off-by: Maximilian Bösing <[email protected]>
@boesing boesing force-pushed the feature/ahead-of-time-factory-creator branch from bd6a362 to aa86639 Compare April 5, 2023 14:42
@boesing boesing changed the title [PoC] ahead of time factory creator Ahead of time factory creator Apr 5, 2023
Signed-off-by: Maximilian Bösing <[email protected]>
@boesing boesing force-pushed the feature/ahead-of-time-factory-creator branch from 22bf5dc to 91a7a82 Compare April 5, 2023 14:54
@boesing boesing requested a review from froschdesign April 5, 2023 16:02
boesing added 2 commits April 6, 2023 00:22
@boesing boesing changed the title Ahead of time factory creator Ahead of time factory creation command Apr 5, 2023
Signed-off-by: Maximilian Bösing <[email protected]>
boesing added 16 commits April 9, 2023 00:03
Signed-off-by: Maximilian Bösing <[email protected]>
…onstructorParameterResolver`

The `ReflectionBasedAbstractFactory` should only test itself rather than integration testing the constructor parameter resolver. Tests which verified the appropriate functionality from `ConstructorParameterResolver` were moved to a dedicated test class.

Signed-off-by: Maximilian Bösing <[email protected]>
In case there are autoloadable factories with the same class-name as the generated  factory, the factory generation should be aborted to avoid autolaoding conflicts.

Signed-off-by: Maximilian Bösing <[email protected]>
Signed-off-by: Maximilian Bösing <[email protected]>
@boesing boesing requested a review from froschdesign April 9, 2023 22:33
Copy link
Member

@froschdesign froschdesign left a comment

Choose a reason for hiding this comment

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

The documentation is good, I will take over all further changes, because it would go beyond the scope here.

@boesing boesing force-pushed the feature/ahead-of-time-factory-creator branch from 50e72c7 to 89976b0 Compare April 11, 2023 17:28
@boesing boesing merged commit ddd5ae9 into laminas:4.0.x Apr 16, 2023
@boesing boesing deleted the feature/ahead-of-time-factory-creator branch April 16, 2023 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AOT factory creation CLI command for ReflectionBasedAbstractFactory mapped factories
3 participants