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
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
aa86639
feature: introduce ahead-of-time factory compiler
boesing Feb 24, 2023
04b41a7
qa: add native return-types to `ReflectionBasedAbstractFactory` methods
boesing Apr 5, 2023
91a7a82
qa: mark CLI command as internal
boesing Apr 5, 2023
0bd48d6
docs: add documentation for `Ahead of Time Factories`
boesing Apr 5, 2023
595d69c
docs: re-order menu items so that there is a `Reflection-based Abstra…
boesing Apr 5, 2023
5363f9e
docs: enhance documentation regarding CI pipeline
boesing Apr 5, 2023
124d530
docs: versionize documentation
boesing Apr 5, 2023
95ba1f0
docs: reference correct file so that mkdocs will generate proper link…
boesing Apr 5, 2023
9216f52
docs: remove superfluous backtick
boesing Apr 8, 2023
86b33a8
docs: remove superfluous whitespaces
boesing Apr 8, 2023
d35caf5
docs: normalize some parts of the migration guide to match markdown r…
boesing Apr 8, 2023
6c5361b
docs: purge migration guide for v4
boesing Apr 8, 2023
f0e62cb
docs: normalize ahead-of-time-factories.md to match markdown rules
boesing Apr 8, 2023
32d6cc9
docs: remove trailing punctuation
boesing Apr 8, 2023
178c011
qa: mark `AheadOfTimeCompiledFactory#__construct` as internal
boesing Apr 8, 2023
c077566
qa: verify that `localConfigFilename` is actually writable
boesing Apr 8, 2023
be9edd7
qa: only allow array configuration to be passed
boesing Apr 8, 2023
eb58add
qa: add unit tests for `AheadOfTimeFactoryCreatorCommand`
boesing Apr 8, 2023
227eb72
qa: modify config type to `array`
boesing Apr 8, 2023
766b470
docs: add dedicated console documentation for the ahead of time facto…
boesing Apr 8, 2023
4ef0191
refactor: early return invokable when there are no required parameters
boesing Apr 9, 2023
b949deb
qa: apply coding standard to `AheadOfTimeFactoryCompiler`
boesing Apr 9, 2023
da333d7
refactor: move unit tests from `ReflectionBasedAbstractFactory` to `C…
boesing Apr 9, 2023
dc82a21
qa: move more complex tools into dedicated namespaces
boesing Apr 9, 2023
e7827c4
qa: add failing test for already existing factories
boesing Apr 9, 2023
b6946b1
feature: detect already existing factories
boesing Apr 9, 2023
b9675b2
qa: do not allow factory compilation for enums
boesing Apr 9, 2023
151d34d
qa: add tests for the `AheadOfTimeFactoryCompiler`
boesing Apr 9, 2023
334a86e
qa: add some more unit tests for `AheadOfTimeFactoryCompiler`
boesing Apr 9, 2023
3613baa
qa: add enum issues to psalm baseline
boesing Apr 9, 2023
9035a2d
qa: fix line-length coding-standard issue
boesing Apr 9, 2023
89976b0
docs: remove superfluous whitespace at the end of the line
boesing Apr 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"require": {
"php": "~8.0.0 || ~8.1.0 || ~8.2.0",
"brick/varexporter": "^0.3.8",
"laminas/laminas-stdlib": "^3.2.1",
"psr/container": "^1.0"
},
Expand All @@ -49,6 +50,7 @@
"laminas/laminas-coding-standard": "~2.5.0",
"laminas/laminas-container-config-test": "^0.8",
"laminas/laminas-dependency-plugin": "^2.2",
"lctrs/psalm-psr-container-plugin": "^1.9",
"mikey179/vfsstream": "^1.6.11@alpha",
"ocramius/proxy-manager": "^2.14.1",
"phpbench/phpbench": "^1.2.7",
Expand Down
241 changes: 184 additions & 57 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions docs/book/ahead-of-time-factories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Ahead of Time Factories
froschdesign marked this conversation as resolved.
Show resolved Hide resolved

- Since 4.0.0

In addition to the already existing [Reflection Factory](TODO), one can create factories for those services using `ReflectionBasedAbstractFactory` before deploying the project to production.
boesing marked this conversation as resolved.
Show resolved Hide resolved
For this purpose, a `laminas-cli` command was created. Therefore, `laminas/laminas-cli` is required as at least a `require-dev` dependency.
Using `ReflectionBasedAbstractFactory` in production is not recommended as the usage of `Reflection` is not too performant.

## Usage

It is recommended to create factories within CI pipeline. While developing a service, the `ReflectionBasedAbstractFactory` can help to dynamically extend the constructor without the need of regenerating already created/generated factories.

To generate the factories, run the following CLI command after [setting up the project](#project-setup):

```
$ php vendor/bin/laminas servicemanager:generate-aot-factories [<target for generated factory config>]
```

The CLI command will then scan your whole configuration for **every** container/plugin-manager look-a-like service configuration where services are using `ReflectionBasedAbstractFactory` as their factory.
Wherever `ReflectionBasedAbstractFactory` is used within a `factories` config entry, the CLI command will generate a factory while adding the replacement to the generated factory config.

When the CLI command has finished, there are all factories generated within the path (`ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH`) registered in the projects configuration along with the `<target for generated factory config>` file (defaults to `config/autoload/generated-factories.local.php`). It is required to run `composer dump-autoload` (in case you've used optimized/classmap-authoritative flag, you should pass these here again) after executing the CLI command as the autoloader has to pick up the generated factory classes. In case of an existing config cache, it is also mandatory to remove that cached configuration file.

When the project is executed having all the files in-place, the generated factory classes are picked up instead of the `ReflectionBasedAbstractFactory` and thus, no additional runtime side-effects based on `Reflection` will occur.

Ensure that both `<target for generated factory config>` file and the directory (including sub-directories and files) configured within `ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH`` is being picked up when generating the artifact which is deployed to production.

## Project Setup

The project needs some additional configuration so that the generated factories are properly detected and registered.

### Additional Composer Dependencies

To execute the CLI command which auto-detects all services using the `ReflectionBasedAbstractFactory`, `laminas/laminas-cli` needs to be added as at least a dev requirement.
There is no TODO in case that `laminas/laminas-cli` is already available in the project.

```
$ composer require --dev laminas/laminas-cli
```

### Configuration

The configuration needs an additional configuration key which provides the target on where the generated factory classes should be stored.
One should use the `CONFIGURATION_KEY_FACTORY_TARGET_PATH` constant from `\Laminas\ServiceManager\ConfigProvider` for this.
Use either `config/autoload/global.php` (which might already exist) or the `Application`-Module configuration (`Application\Module#getConfig` or `Application\ConfigProvider#__invoke`) to do so.

Both Laminas-MVC and Mezzio do share the configuration directory structure as follows:

```
.
├── config
│   ├── autoload
│   │   ├── global.php
│   │   └── local.php.dist
└── data
```

#### Generated Factories Location

To avoid namespace conflicts with existing modules, it is recommended to create a dedicated directory under `data` which can be used as the target directory for the generated factories.
For example: `data/GeneratedServiceManagerFactories`. This directory should contain either `.gitkeep` (in case you prefer to commit your generated factories) and/or a `.gitignore` which excludes all PHP files from being committed to your project. After adding either `.gittkeep` or `.gitignore`, head to the projects `composer.json` and add (if not yet exists) `classmap` to the `autoload` section. Within that `classmap` property, target the recently created directory where the factories are meant to be stored:

```json
{
"name": "vendor/project",
"type": "project",
"[...]": {},
"autoload": {
"classmap": ["data/GeneratedServiceManagerFactories"]
}
}
```

This will provide composer with the information, that PHP classes can be found within that directory and thus, all classes are automatically dumped on `composer dump-autoload` for example.

#### Configuration overrides

> ### Configuration merge strategy
>
> The `autoload` config folder is scanned for files named `[<whatever>]<environment|global|local>.php`.
> Those files containing `[*.]local.php` are ignored via `.gitignore` so that these are not accidentally committed.
> The configuration merge will happen in the following order:
> 1. global configurations are used first
> 2. global configurations are overridden by environment specific configurations
> 3. global and environment specific configurations are overridden by local configurations

The CLI command to generate the factories expects a path to a file, which will be created (or overridden) and which will contain **all** service <=> factory entries for the projects container and plugin-managers.

For example, if the CLI command detects `Laminas-MVC` `service_manager` service and `laminas/laminas-validator` validators using `ReflectionBasedAbstractFactory`, it will create a file like this:

```php
return [
'service_manager' => [
'factories' => [
MyService::class => GeneratedMyServiceFactory::class,
],
],
'validators' => [
'factories' => [
MyValidator::class => GeneratedMyValidatorFactory::class,
],
],
];
```

So the default location of the generated configuration which should automatically replace existing configuration (containing `ReflectionBasedAbstractFactory`) is targeted to `config/autoload/generated-factories.local.php`. Local configuration files will always replace global/environment/module configurations and therefore, it perfectly fit our needs.
Loading