From aa866392969c15273a98725841a7c2c34a8bf678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Fri, 24 Feb 2023 02:35:21 +0100 Subject: [PATCH 01/32] feature: introduce ahead-of-time factory compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 <2189546+boesing@users.noreply.github.com> --- composer.json | 2 + composer.lock | 241 +++++++++++++----- psalm.xml.dist | 1 + .../ReflectionBasedAbstractFactory.php | 166 ++---------- .../AheadOfTimeFactoryCreatorCommand.php | 133 ++++++++++ ...headOfTimeFactoryCreatorCommandFactory.php | 38 +++ src/ConfigProvider.php | 27 +- src/Exception/RuntimeException.php | 11 + src/Tool/AheadOfTimeCompiledFactory.php | 20 ++ src/Tool/AheadOfTimeFactoryCompiler.php | 106 ++++++++ .../AheadOfTimeFactoryCompilerFactory.php | 15 ++ .../AheadOfTimeFactoryCompilerInterface.php | 13 + src/Tool/ConstructorParameterResolver.php | 174 +++++++++++++ .../ConstructorParameterResolverInterface.php | 37 +++ src/Tool/FactoryCreator.php | 146 ++++++----- src/Tool/FactoryCreatorFactory.php | 18 ++ src/Tool/FactoryCreatorInterface.php | 3 +- src/Tool/FallbackConstructorParameter.php | 13 + ...rviceFromContainerConstructorParameter.php | 16 ++ .../factories/ComplexDependencyObject.php | 1 - test/TestAsset/factories/InvokableObject.php | 3 +- .../factories/SimpleDependencyObject.php | 1 - test/Tool/FactoryCreatorTest.php | 47 +++- 23 files changed, 948 insertions(+), 284 deletions(-) create mode 100644 src/Command/AheadOfTimeFactoryCreatorCommand.php create mode 100644 src/Command/AheadOfTimeFactoryCreatorCommandFactory.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Tool/AheadOfTimeCompiledFactory.php create mode 100644 src/Tool/AheadOfTimeFactoryCompiler.php create mode 100644 src/Tool/AheadOfTimeFactoryCompilerFactory.php create mode 100644 src/Tool/AheadOfTimeFactoryCompilerInterface.php create mode 100644 src/Tool/ConstructorParameterResolver.php create mode 100644 src/Tool/ConstructorParameterResolverInterface.php create mode 100644 src/Tool/FactoryCreatorFactory.php create mode 100644 src/Tool/FallbackConstructorParameter.php create mode 100644 src/Tool/ServiceFromContainerConstructorParameter.php diff --git a/composer.json b/composer.json index bd0236f6..d6a5cacc 100644 --- a/composer.json +++ b/composer.json @@ -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" }, @@ -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", diff --git a/composer.lock b/composer.lock index 3e4fa2df..38db349e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,57 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7bba09ab6e84159c92c4425f2bfe37a3", + "content-hash": "b6ac4e51e97fa9ed48da4804d35b859c", "packages": [ + { + "name": "brick/varexporter", + "version": "0.3.8", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/b5853edea6204ff8fa10633c3a4cccc4058410ed", + "reference": "b5853edea6204ff8fa10633c3a4cccc4058410ed", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5 || ^9.0", + "vimeo/psalm": "4.23.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.3.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-21T23:05:38+00:00" + }, { "name": "laminas/laminas-stdlib", "version": "3.16.1", @@ -65,6 +114,62 @@ ], "time": "2022-12-03T18:48:01+00:00" }, + { + "name": "nikic/php-parser", + "version": "v4.15.3", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + }, + "time": "2023-01-16T22:05:37+00:00" + }, { "name": "psr/container", "version": "1.1.2", @@ -1481,6 +1586,84 @@ ], "time": "2021-09-08T17:51:35+00:00" }, + { + "name": "lctrs/psalm-psr-container-plugin", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/Lctrs/psalm-psr-container-plugin.git", + "reference": "2a3608a19a555a1589c12a97ff6f814a780def48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Lctrs/psalm-psr-container-plugin/zipball/2a3608a19a555a1589c12a97ff6f814a780def48", + "reference": "2a3608a19a555a1589c12a97ff6f814a780def48", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "nikic/php-parser": "^4.15.2", + "php": ">=8.0.2 <8.3.0", + "psr/container": "^1.1.2 || ^2.0.2", + "vimeo/psalm": "^5.0.0" + }, + "require-dev": { + "codeception/codeception": "4.2.2", + "codeception/module-asserts": "2.0.1", + "codeception/module-cli": "1.1.1", + "codeception/module-filesystem": "1.0.3", + "doctrine/coding-standard": "9.0.0", + "ergebnis/composer-normalize": "2.28.3", + "ergebnis/license": "2.1.0", + "phpstan/extension-installer": "1.2.0", + "phpstan/phpstan": "1.9.2", + "phpstan/phpstan-deprecation-rules": "1.0.0", + "phpstan/phpstan-phpunit": "1.2.2", + "phpstan/phpstan-strict-rules": "1.4.4", + "phpunit/phpunit": "9.5.26", + "psalm/plugin-phpunit": "0.18.3", + "symfony/yaml": "5.4.16", + "weirdan/codeception-psalm-module": "0.13.1" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Lctrs\\PsalmPsrContainerPlugin\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Lctrs\\PsalmPsrContainerPlugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jérôme Parmentier", + "email": "jerome@prmntr.me" + } + ], + "description": "Let Psalm understand better psr11 containers", + "homepage": "https://github.com/Lctrs/psalm-psr-container-plugin", + "keywords": [ + "code", + "container", + "inspection", + "php", + "psalm", + "psalm-plugin", + "psr", + "psr11" + ], + "support": { + "issues": "https://github.com/Lctrs/psalm-psr-container-plugin/issues", + "source": "https://github.com/Lctrs/psalm-psr-container-plugin" + }, + "time": "2022-12-01T07:02:46+00:00" + }, { "name": "mikey179/vfsstream", "version": "v1.6.11", @@ -1642,62 +1825,6 @@ }, "time": "2022-12-08T20:46:14+00:00" }, - { - "name": "nikic/php-parser", - "version": "v4.15.2", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" - }, - "time": "2022-11-12T15:38:23+00:00" - }, { "name": "ocramius/proxy-manager", "version": "2.14.1", diff --git a/psalm.xml.dist b/psalm.xml.dist index 220b419a..78f1b869 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -63,5 +63,6 @@ + diff --git a/src/AbstractFactory/ReflectionBasedAbstractFactory.php b/src/AbstractFactory/ReflectionBasedAbstractFactory.php index 4f4654bd..254b8ebc 100644 --- a/src/AbstractFactory/ReflectionBasedAbstractFactory.php +++ b/src/AbstractFactory/ReflectionBasedAbstractFactory.php @@ -4,17 +4,14 @@ namespace Laminas\ServiceManager\AbstractFactory; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Exception\InvalidArgumentException; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; use Psr\Container\ContainerInterface; use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use function array_map; use function class_exists; -use function interface_exists; -use function is_string; use function sprintf; /** @@ -67,66 +64,38 @@ * * Based on the LazyControllerAbstractFactory from laminas-mvc. */ -class ReflectionBasedAbstractFactory implements AbstractFactoryInterface +final class ReflectionBasedAbstractFactory implements AbstractFactoryInterface { - /** - * Maps known classes/interfaces to the service that provides them; only - * required for those services with no entry based on the class/interface - * name. - * - * Extend the class if you wish to add to the list. - * - * Example: - * - * - * [ - * \Laminas\Filter\FilterPluginManager::class => 'FilterManager', - * \Laminas\Validator\ValidatorPluginManager::class => 'ValidatorManager', - * ] - * - * - * @var string[] - */ - protected $aliases = []; + private ConstructorParameterResolverInterface $constructorParameterResolver; /** * Allows overriding the internal list of aliases. These should be of the * form `class name => well-known service name`; see the documentation for * the `$aliases` property for details on what is accepted. * - * @param string[] $aliases + * @param array $aliases */ - public function __construct(array $aliases = []) - { - if (! empty($aliases)) { - $this->aliases = $aliases; - } + public function __construct( + public array $aliases = [], + ?ConstructorParameterResolverInterface $constructorParameterResolver = null, + ) { + $this->constructorParameterResolver = $constructorParameterResolver ?? new ConstructorParameterResolver(); } /** * {@inheritDoc} - * - * @return DispatchableInterface */ public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) { - $reflectionClass = new ReflectionClass($requestedName); - - if (null === ($constructor = $reflectionClass->getConstructor())) { - return new $requestedName(); + if (! class_exists($requestedName)) { + throw new InvalidArgumentException(sprintf('%s can only be used with class names.', self::class)); } - $reflectionParameters = $constructor->getParameters(); - - if (empty($reflectionParameters)) { - return new $requestedName(); - } - - $resolver = $container->has('config') - ? $this->resolveParameterWithConfigService($container, $requestedName) - : $this->resolveParameterWithoutConfigService($container, $requestedName); - - $parameters = array_map($resolver, $reflectionParameters); + $parameters = $this->constructorParameterResolver->resolveConstructorParameters( + $requestedName, + $container, + $this->aliases + ); return new $requestedName(...$parameters); } @@ -143,103 +112,4 @@ private function canCallConstructor(string $requestedName): bool return $constructor === null || $constructor->isPublic(); } - - /** - * Resolve a parameter to a value. - * - * Returns a callback for resolving a parameter to a value, but without - * allowing mapping array `$config` arguments to the `config` service. - * - * @param string $requestedName - * @return callable - */ - private function resolveParameterWithoutConfigService(ContainerInterface $container, $requestedName) - { - /** - * @param ReflectionParameter $parameter - * @return mixed - * @throws ServiceNotFoundException If type-hinted parameter cannot be - * resolved to a service in the container. - * @psalm-suppress MissingClosureReturnType - */ - return fn(ReflectionParameter $parameter) => $this->resolveParameter($parameter, $container, $requestedName); - } - - /** - * Returns a callback for resolving a parameter to a value, including mapping 'config' arguments. - * - * Unlike resolveParameter(), this version will detect `$config` array - * arguments and have them return the 'config' service. - * - * @param string $requestedName - * @return callable - */ - private function resolveParameterWithConfigService(ContainerInterface $container, $requestedName) - { - /** - * @param ReflectionParameter $parameter - * @return mixed - * @throws ServiceNotFoundException If type-hinted parameter cannot be - * resolved to a service in the container. - */ - return function (ReflectionParameter $parameter) use ($container, $requestedName) { - if ($parameter->getName() === 'config') { - $type = $parameter->getType(); - if ($type instanceof ReflectionNamedType && $type->getName() === 'array') { - return $container->get('config'); - } - } - return $this->resolveParameter($parameter, $container, $requestedName); - }; - } - - /** - * Logic common to all parameter resolution. - * - * @param string $requestedName - * @return mixed - * @throws ServiceNotFoundException If type-hinted parameter cannot be - * resolved to a service in the container. - */ - private function resolveParameter(ReflectionParameter $parameter, ContainerInterface $container, $requestedName) - { - $type = $parameter->getType(); - $type = $type instanceof ReflectionNamedType ? $type->getName() : null; - - if ($type === 'array') { - return []; - } - - if ($type === null || (is_string($type) && ! class_exists($type) && ! interface_exists($type))) { - if (! $parameter->isDefaultValueAvailable()) { - throw new ServiceNotFoundException(sprintf( - 'Unable to create service "%s"; unable to resolve parameter "%s" ' - . 'to a class, interface, or array type', - $requestedName, - $parameter->getName() - )); - } - - return $parameter->getDefaultValue(); - } - - $type = $this->aliases[$type] ?? $type; - - if ($container->has($type)) { - return $container->get($type); - } - - if (! $parameter->isOptional()) { - throw new ServiceNotFoundException(sprintf( - 'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"', - $requestedName, - $parameter->getName(), - $type - )); - } - - // Type not available in container, but the value is optional and has a - // default defined. - return $parameter->getDefaultValue(); - } } diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php new file mode 100644 index 00000000..9a7b4b80 --- /dev/null +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -0,0 +1,133 @@ +setDescription( + 'Creates factories which replace the runtime overhead for `ReflectionBasedAbstractFactory`.' + ); + $this->addArgument( + 'localConfigFilename', + InputArgument::OPTIONAL, + '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.', + 'config/autoload/generated-factories.local.php', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($this->factoryTargetPath === '' || ! is_writable($this->factoryTargetPath)) { + $output->writeln(sprintf( + 'Please configure the `%s` configuration key in your projects config and ensure that the' + . ' directory is registered to the composer autoloader using `classmap` and writable by the executing' + . ' user.', + ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH, + )); + + return self::FAILURE; + } + + $localConfigFilename = $input->getArgument('localConfigFilename'); + assert(is_string($localConfigFilename)); + + $compiledFactories = $this->factoryCompiler->compile($this->config); + if ($compiledFactories === []) { + $output->writeln( + 'There is no (more) service registered to use the `ReflectionBasedAbstractFactory`.' + ); + + return self::SUCCESS; + } + + $containerConfigurations = []; + + foreach ($compiledFactories as $factory) { + $targetDirectory = sprintf( + '%s/%s', + $this->factoryTargetPath, + preg_replace('/\W/', '', $factory->containerConfigurationKey) + ); + + if (! is_dir($targetDirectory)) { + if (! mkdir($targetDirectory, recursive: true) && ! is_dir($targetDirectory)) { + throw new RuntimeException(sprintf('Unable to create directory "%s".', $targetDirectory)); + } + } + + /** @var class-string $factoryClassName */ + $factoryClassName = sprintf('%sFactory', $factory->fullyQualifiedClassName); + $factoryFileName = sprintf( + '%s/%s.php', + $targetDirectory, + str_replace('\\', '_', $factoryClassName) + ); + file_put_contents($factoryFileName, $factory->generatedFactory); + if (! isset($containerConfigurations[$factory->containerConfigurationKey])) { + $containerConfigurations[$factory->containerConfigurationKey] = ['factories' => []]; + } + + $containerConfigurations[$factory->containerConfigurationKey]['factories'] += [ + $factory->fullyQualifiedClassName => $factoryClassName, + ]; + } + + file_put_contents($localConfigFilename, $this->createLocalAotContainerConfigContent($containerConfigurations)); + + $output->writeln(sprintf('Successfully created %d factories.', count($compiledFactories))); + return self::SUCCESS; + } + + /** + * @param non-empty-array $containerConfigurations + * @return non-empty-string + */ + private function createLocalAotContainerConfigContent(array $containerConfigurations): string + { + return sprintf('get(AheadOfTimeFactoryCompilerInterface::class); + $config = $container->has('config') ? $container->get('config') : []; + if (! is_iterable($config)) { + return new AheadOfTimeFactoryCreatorCommand([], '', $aheadOfTimeFactoryCompiler); + } + + if (! is_array($config)) { + $config = iterator_to_array($config); + } + + /** @psalm-suppress MixedAssignment Even tho we do verify the type right after assigning it, psalm has problems with that*/ + $factoryTargetPath = $config[ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH] ?? ''; + if (! is_string($factoryTargetPath)) { + $factoryTargetPath = ''; + } + + return new AheadOfTimeFactoryCreatorCommand($config, $factoryTargetPath, $aheadOfTimeFactoryCompiler); + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 5bc5cba8..4816f156 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -4,12 +4,18 @@ namespace Laminas\ServiceManager; +use Laminas\ServiceManager\Command\AheadOfTimeFactoryCreatorCommand; +use Laminas\ServiceManager\Command\AheadOfTimeFactoryCreatorCommandFactory; use Laminas\ServiceManager\Command\ConfigDumperCommand; use Laminas\ServiceManager\Command\FactoryCreatorCommand; use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerFactory; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerInterface; use Laminas\ServiceManager\Tool\ConfigDumperFactory; use Laminas\ServiceManager\Tool\ConfigDumperInterface; -use Laminas\ServiceManager\Tool\FactoryCreator; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; +use Laminas\ServiceManager\Tool\FactoryCreatorFactory; use Laminas\ServiceManager\Tool\FactoryCreatorInterface; use Symfony\Component\Console\Command\Command; @@ -20,6 +26,8 @@ */ final class ConfigProvider { + public const CONFIGURATION_KEY_FACTORY_TARGET_PATH = 'aot-factory-target-path'; + /** * @return array{ * dependencies: ServiceManagerConfigurationType, @@ -40,14 +48,18 @@ public function __invoke(): array public function getServiceDependencies(): array { $factories = [ - ConfigDumperInterface::class => ConfigDumperFactory::class, - FactoryCreatorInterface::class => static fn (): FactoryCreatorInterface => new FactoryCreator(), + ConfigDumperInterface::class => ConfigDumperFactory::class, + FactoryCreatorInterface::class => FactoryCreatorFactory::class, + AheadOfTimeFactoryCompilerInterface::class => AheadOfTimeFactoryCompilerFactory::class, + ConstructorParameterResolverInterface::class => static fn (): ConstructorParameterResolverInterface + => new ConstructorParameterResolver(), ]; if (class_exists(Command::class)) { $factories += [ - ConfigDumperCommand::class => InvokableFactory::class, - FactoryCreatorCommand::class => InvokableFactory::class, + AheadOfTimeFactoryCreatorCommand::class => AheadOfTimeFactoryCreatorCommandFactory::class, + ConfigDumperCommand::class => InvokableFactory::class, + FactoryCreatorCommand::class => InvokableFactory::class, ]; } @@ -67,8 +79,9 @@ private function getLaminasCliDependencies(): array return [ 'commands' => [ - ConfigDumperCommand::NAME => ConfigDumperCommand::class, - FactoryCreatorCommand::NAME => FactoryCreatorCommand::class, + ConfigDumperCommand::NAME => ConfigDumperCommand::class, + FactoryCreatorCommand::NAME => FactoryCreatorCommand::class, + AheadOfTimeFactoryCreatorCommand::NAME => AheadOfTimeFactoryCreatorCommand::class, ], ]; } diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 00000000..d94d037f --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,11 @@ +extractServicesRegisteredByReflectionBasedFactory( + $config + ); + + $compiledFactories = []; + + foreach ($servicesRegisteredByReflectionBasedFactory as $service => [$containerConfigurationKey, $aliases]) { + $compiledFactories[] = new AheadOfTimeCompiledFactory( + $service, + $containerConfigurationKey, + $this->factoryCreator->createFactory($service, $aliases), + ); + } + + return $compiledFactories; + } + + /** + * @param iterable $config + * @return array}> + */ + private function extractServicesRegisteredByReflectionBasedFactory(iterable $config): array + { + $services = []; + + foreach ($config as $key => $entry) { + if (! is_array($entry)) { + continue; + } + + if (! array_key_exists('factories', $entry) || ! is_array($entry['factories'])) { + continue; + } + + /** @var array> $servicesUsingReflectionBasedFactory */ + $servicesUsingReflectionBasedFactory = array_filter( + $entry['factories'], + static fn(mixed $value): bool => + $value === ReflectionBasedAbstractFactory::class + || $value instanceof ReflectionBasedAbstractFactory, + ARRAY_FILTER_USE_BOTH, + ); + + if ($servicesUsingReflectionBasedFactory === []) { + continue; + } + + assert(is_string($key) && $key !== ''); + + foreach ($servicesUsingReflectionBasedFactory as $service => $factory) { + if (! class_exists($service)) { + throw new InvalidArgumentException(sprintf( + 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist.', + $service + )); + } + + if (isset($services[$service])) { + throw new InvalidArgumentException(sprintf( + 'The exact same service "%s" is registered in (at least) two service-/plugin-managers: %s, %s', + $service, + $services[$service][0], + $key + )); + } + + $aliases = []; + if ($factory instanceof ReflectionBasedAbstractFactory && $factory->aliases !== []) { + $aliases = $factory->aliases; + } + + $services[$service] = [$key, $aliases]; + } + } + + return $services; + } +} diff --git a/src/Tool/AheadOfTimeFactoryCompilerFactory.php b/src/Tool/AheadOfTimeFactoryCompilerFactory.php new file mode 100644 index 00000000..8a13ea25 --- /dev/null +++ b/src/Tool/AheadOfTimeFactoryCompilerFactory.php @@ -0,0 +1,15 @@ +get(FactoryCreatorInterface::class)); + } +} diff --git a/src/Tool/AheadOfTimeFactoryCompilerInterface.php b/src/Tool/AheadOfTimeFactoryCompilerInterface.php new file mode 100644 index 00000000..4811ca7e --- /dev/null +++ b/src/Tool/AheadOfTimeFactoryCompilerInterface.php @@ -0,0 +1,13 @@ + + */ + public function compile(iterable $config): array; +} diff --git a/src/Tool/ConstructorParameterResolver.php b/src/Tool/ConstructorParameterResolver.php new file mode 100644 index 00000000..9e00fca7 --- /dev/null +++ b/src/Tool/ConstructorParameterResolver.php @@ -0,0 +1,174 @@ +resolveConstructorParameterServiceNamesOrFallbackTypes($className, $container, $aliases); + + return array_map(static function ( + FallbackConstructorParameter|ServiceFromContainerConstructorParameter $parameter + ) use ($container): mixed { + if ($parameter instanceof FallbackConstructorParameter) { + return $parameter->argumentValue; + } + + return $container->get($parameter->serviceName); + }, $parameters); + } + + /** + * Resolve a parameter to a value. + * + * Returns a callback for resolving a parameter to a value, but without + * allowing mapping array `$config` arguments to the `config` service. + * + * @param class-string $className + * @param array $aliases + * @return callable(ReflectionParameter):(FallbackConstructorParameter|ServiceFromContainerConstructorParameter) + */ + private function resolveParameterWithoutConfigService( + ContainerInterface $container, + string $className, + array $aliases + ): callable { + return fn(ReflectionParameter $parameter): FallbackConstructorParameter|ServiceFromContainerConstructorParameter + => $this->resolveParameter($parameter, $container, $className, $aliases); + } + + /** + * Returns a callback for resolving a parameter to a value, including mapping 'config' arguments. + * + * Unlike resolveParameter(), this version will detect `$config` array + * arguments and have them return the 'config' service. + * + * @param class-string $className + * @param array $aliases + * @return callable(ReflectionParameter):(FallbackConstructorParameter|ServiceFromContainerConstructorParameter) + */ + private function resolveParameterWithConfigService( + ContainerInterface $container, + string $className, + array $aliases + ): callable { + return function ( + ReflectionParameter $parameter + ) use ( + $container, + $className, + $aliases + ): FallbackConstructorParameter|ServiceFromContainerConstructorParameter { + if ($parameter->getName() === 'config') { + $type = $parameter->getType(); + if ($type instanceof ReflectionNamedType && $type->getName() === 'array') { + return new ServiceFromContainerConstructorParameter('config'); + } + } + return $this->resolveParameter($parameter, $container, $className, $aliases); + }; + } + + /** + * Logic common to all parameter resolution. + * + * @param class-string $className + * @param array $aliases + * @throws ServiceNotFoundException If type-hinted parameter cannot be + * resolved to a service in the container. + */ + private function resolveParameter( + ReflectionParameter $parameter, + ContainerInterface $container, + string $className, + array $aliases + ): FallbackConstructorParameter|ServiceFromContainerConstructorParameter { + $type = $parameter->getType(); + $type = $type instanceof ReflectionNamedType ? $type->getName() : null; + + if ($type === 'array') { + return new FallbackConstructorParameter([]); + } + + if ($type === null || (! class_exists($type) && ! interface_exists($type))) { + if (! $parameter->isDefaultValueAvailable()) { + throw new ServiceNotFoundException(sprintf( + 'Unable to create service "%s"; unable to resolve parameter "%s" ' + . 'to a class, interface, or array type', + $className, + $parameter->getName() + )); + } + + return new FallbackConstructorParameter($parameter->getDefaultValue()); + } + + $type = $aliases[$type] ?? $type; + + if ($container->has($type)) { + assert($type !== ''); + return new ServiceFromContainerConstructorParameter($type); + } + + if (! $parameter->isOptional()) { + throw new ServiceNotFoundException(sprintf( + 'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"', + $className, + $parameter->getName(), + $type + )); + } + + // Type not available in container, but the value is optional and has a + // default defined. + return new FallbackConstructorParameter($parameter->getDefaultValue()); + } + + /** {@inheritDoc} */ + public function resolveConstructorParameterServiceNamesOrFallbackTypes( + string $className, + ContainerInterface $container, + array $aliases, + ): array { + $reflectionClass = new ReflectionClass($className); + + $constructor = $reflectionClass->getConstructor(); + if (null === $constructor) { + return []; + } + + $reflectionParameters = $constructor->getParameters(); + + if ($reflectionParameters === []) { + return []; + } + + $resolver = $container->has('config') + ? $this->resolveParameterWithConfigService($container, $className, $aliases) + : $this->resolveParameterWithoutConfigService($container, $className, $aliases); + + return array_map($resolver, $reflectionParameters); + } +} diff --git a/src/Tool/ConstructorParameterResolverInterface.php b/src/Tool/ConstructorParameterResolverInterface.php new file mode 100644 index 00000000..3e9f725b --- /dev/null +++ b/src/Tool/ConstructorParameterResolverInterface.php @@ -0,0 +1,37 @@ + $aliases + * @return list + */ + public function resolveConstructorParameters( + string $className, + ContainerInterface $container, + array $aliases, + ): array; + + /** + * Returns service names and/or native fallback types which can be either used to retrieve services from container + * or to be passed to the constructor directly. + * + * @param class-string $className + * @param array $aliases + * @return list + */ + public function resolveConstructorParameterServiceNamesOrFallbackTypes( + string $className, + ContainerInterface $container, + array $aliases, + ): array; +} diff --git a/src/Tool/FactoryCreator.php b/src/Tool/FactoryCreator.php index 22026542..a2ea814f 100644 --- a/src/Tool/FactoryCreator.php +++ b/src/Tool/FactoryCreator.php @@ -4,39 +4,40 @@ namespace Laminas\ServiceManager\Tool; -use Laminas\ServiceManager\Exception\InvalidArgumentException; +use Brick\VarExporter\VarExporter; use Laminas\ServiceManager\Factory\FactoryInterface; +use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerInterface; -use ReflectionClass; -use ReflectionNamedType; -use ReflectionParameter; -use function array_filter; use function array_map; -use function array_merge; use function array_shift; use function assert; +use function class_exists; use function count; use function implode; +use function is_string; use function preg_replace; use function sort; use function sprintf; +use function str_contains; use function str_repeat; use function strrpos; use function substr; +use const PHP_EOL; + /** * @internal */ final class FactoryCreator implements FactoryCreatorInterface { + private const NAMESPACE_SEPARATOR = '\\'; + private const FACTORY_TEMPLATE = <<<'EOT' container = $container ?? new ServiceManager(); + $this->constructorParameterResolver = $constructorParameterResolver ?? new ConstructorParameterResolver(); + } + + public function createFactory(string $className, array $aliases = []): string { - $class = $this->getClassName($className); + $class = $this->getClassName($className); + $namespace = $this->getNamespace($className, $class); return sprintf( self::FACTORY_TEMPLATE, - preg_replace('/\\\\' . $class . '$/', '', $className), - $this->createImportStatements($className), + $namespace, + $this->createImportStatements(), $class, $class, $class, - $this->createArgumentString($className) + $this->createArgumentString($className, $aliases) ); } @@ -81,7 +94,7 @@ public function createFactory(string $className): string */ private function getClassName(string $className): string { - $lastNamespaceSeparator = strrpos($className, '\\'); + $lastNamespaceSeparator = strrpos($className, self::NAMESPACE_SEPARATOR); if ($lastNamespaceSeparator === false) { return $className; } @@ -94,67 +107,44 @@ private function getClassName(string $className): string /** * @param class-string $className + * @param array $aliases * @return array */ - private function getConstructorParameters(string $className): array + private function getConstructorParameters(string $className, array $aliases): array { - $reflectionClass = new ReflectionClass($className); - $constructor = $reflectionClass->getConstructor(); - - if ($constructor === null) { - return []; - } - - $constructorParameters = $constructor->getParameters(); + $dependencies = $this->constructorParameterResolver->resolveConstructorParameterServiceNamesOrFallbackTypes( + $className, + $this->container, + $aliases, + ); - if ($constructorParameters === []) { - return []; - } + $stringifiedConstructorArguments = []; - $constructorParameters = array_filter( - $constructorParameters, - static function (ReflectionParameter $argument): bool { - if ($argument->isOptional()) { - return false; - } - - $type = $argument->getType(); - $class = $type instanceof ReflectionNamedType && ! $type->isBuiltin() ? $type->getName() : null; - - if (null === $class) { - throw new InvalidArgumentException(sprintf( - 'Cannot identify type for constructor argument "%s"; ' - . 'no type hint, or non-class/interface type hint', - $argument->getName() - )); - } - - return true; + foreach ($dependencies as $dependency) { + if ($dependency instanceof ServiceFromContainerConstructorParameter) { + $stringifiedConstructorArguments[] = sprintf( + '$container->get(%s)', + $this->export($dependency->serviceName) + ); + continue; } - ); - if ($constructorParameters === []) { - return []; + $stringifiedConstructorArguments[] = $this->export($dependency->argumentValue); } - return array_map(static function (ReflectionParameter $parameter): string { - $type = $parameter->getType(); - // We can safely assert here as the filter above already triggers InvalidArgumentException - assert($type instanceof ReflectionNamedType && ! $type->isBuiltin()); - - return $type->getName(); - }, $constructorParameters); + return $stringifiedConstructorArguments; } /** * @param class-string $className + * @param array $aliases */ - private function createArgumentString(string $className): string + private function createArgumentString(string $className, array $aliases): string { $arguments = array_map( static fn(string $dependency): string - => sprintf('$container->get(\\%s::class)', $dependency), - $this->getConstructorParameters($className) + => sprintf('%s', $dependency), + $this->getConstructorParameters($className, $aliases) ); switch (count($arguments)) { @@ -174,10 +164,40 @@ private function createArgumentString(string $className): string } } - private function createImportStatements(string $className): string + private function createImportStatements(): string { - $imports = array_merge(self::IMPORT_ALWAYS, [$className]); + $imports = self::IMPORT_ALWAYS; sort($imports); return implode("\n", array_map(static fn(string $import): string => sprintf('use %s;', $import), $imports)); } + + private function export(mixed $value): string + { + if (is_string($value) && class_exists($value)) { + return sprintf('\\%s::class', $value); + } + + return VarExporter::export( + $value, + VarExporter::NO_CLOSURES | VarExporter::NO_SERIALIZE | VarExporter::NO_SERIALIZE | VarExporter::NO_SET_STATE + ); + } + + /** + * @param class-string $className + * @param non-empty-string $class + */ + private function getNamespace(string $className, string $class): string + { + if (! str_contains($className, self::NAMESPACE_SEPARATOR)) { + return ''; + } + + return sprintf( + '%snamespace %s;%s', + PHP_EOL, + preg_replace('/\\\\' . $class . '$/', '', $className), + PHP_EOL + ); + } } diff --git a/src/Tool/FactoryCreatorFactory.php b/src/Tool/FactoryCreatorFactory.php new file mode 100644 index 00000000..02880078 --- /dev/null +++ b/src/Tool/FactoryCreatorFactory.php @@ -0,0 +1,18 @@ +get(ConstructorParameterResolverInterface::class)); + } +} diff --git a/src/Tool/FactoryCreatorInterface.php b/src/Tool/FactoryCreatorInterface.php index a1b2424d..433b9755 100644 --- a/src/Tool/FactoryCreatorInterface.php +++ b/src/Tool/FactoryCreatorInterface.php @@ -8,7 +8,8 @@ interface FactoryCreatorInterface { /** * @param class-string $className + * @param array $aliases * @return non-empty-string */ - public function createFactory(string $className): string; + public function createFactory(string $className, array $aliases = []): string; } diff --git a/src/Tool/FallbackConstructorParameter.php b/src/Tool/FallbackConstructorParameter.php new file mode 100644 index 00000000..5d0ced09 --- /dev/null +++ b/src/Tool/FallbackConstructorParameter.php @@ -0,0 +1,13 @@ +factoryCreator = new FactoryCreator(); + $this->container = $this->createMock(ContainerInterface::class); + $this->factoryCreator = new FactoryCreator( + $this->container, + ); } public function testCreateFactoryCreatesForInvokable(): void @@ -43,6 +52,12 @@ public function testCreateFactoryCreatesForSimpleDependencies(): void { $className = SimpleDependencyObject::class; $factory = file_get_contents(__DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'); + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnMap([ + [InvokableObject::class, true], + ]); self::assertSame($factory, $this->factoryCreator->createFactory($className)); } @@ -52,6 +67,14 @@ public function testCreateFactoryCreatesForComplexDependencies(): void $className = ComplexDependencyObject::class; $factory = file_get_contents(__DIR__ . '/../TestAsset/factories/ComplexDependencyObject.php'); + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnMap([ + [SimpleDependencyObject::class, true], + [SecondComplexDependencyObject::class, true], + ]); + self::assertSame($factory, $this->factoryCreator->createFactory($className)); } @@ -60,9 +83,25 @@ public function testNamespaceGeneration(): void $testClassNames = [ ComplexDependencyObject::class => 'LaminasTest\\ServiceManager\\TestAsset', TargetObjectDelegator::class => 'LaminasTest\\ServiceManager\\TestAsset\\DelegatorAndAliasBehaviorTest', + stdClass::class => '', ]; + + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnMap([ + [SimpleDependencyObject::class, true], + [SecondComplexDependencyObject::class, true], + ]); + foreach ($testClassNames as $testFqcn => $expectedNamespace) { $generatedFactory = $this->factoryCreator->createFactory($testFqcn); + + if ($expectedNamespace === '') { + self::assertStringNotContainsString(PHP_EOL . 'namespace ', $generatedFactory); + continue; + } + preg_match('/^namespace\s([^;]+)/m', $generatedFactory, $namespaceMatch); self::assertNotEmpty($namespaceMatch); From 04b41a7ee0e912f21e52c4cfb44289fe347707de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:47:38 +0200 Subject: [PATCH 02/32] qa: add native return-types to `ReflectionBasedAbstractFactory` methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/AbstractFactory/ReflectionBasedAbstractFactory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AbstractFactory/ReflectionBasedAbstractFactory.php b/src/AbstractFactory/ReflectionBasedAbstractFactory.php index 254b8ebc..27f078e5 100644 --- a/src/AbstractFactory/ReflectionBasedAbstractFactory.php +++ b/src/AbstractFactory/ReflectionBasedAbstractFactory.php @@ -85,7 +85,7 @@ public function __construct( /** * {@inheritDoc} */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): object { if (! class_exists($requestedName)) { throw new InvalidArgumentException(sprintf('%s can only be used with class names.', self::class)); @@ -101,7 +101,7 @@ public function __invoke(ContainerInterface $container, $requestedName, ?array $ } /** {@inheritDoc} */ - public function canCreate(ContainerInterface $container, $requestedName) + public function canCreate(ContainerInterface $container, $requestedName): bool { return class_exists($requestedName) && $this->canCallConstructor($requestedName); } From 91a7a82484c6336283afff22976e9798ba115a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:47:50 +0200 Subject: [PATCH 03/32] qa: mark CLI command as internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Command/AheadOfTimeFactoryCreatorCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index 9a7b4b80..ca4475ff 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -27,6 +27,8 @@ use function str_replace; /** + * @internal CLI commands are not meant to be used in any upstream projects other than via `laminas-cli`. + * * @psalm-import-type ServiceManagerConfigurationType from ConfigInterface */ final class AheadOfTimeFactoryCreatorCommand extends Command From 0bd48d6e74896cfcb5ea5636951a03f70be6ec95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:02:00 +0200 Subject: [PATCH 04/32] docs: add documentation for `Ahead of Time Factories` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/ahead-of-time-factories.md | 105 +++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 106 insertions(+) create mode 100644 docs/book/ahead-of-time-factories.md diff --git a/docs/book/ahead-of-time-factories.md b/docs/book/ahead-of-time-factories.md new file mode 100644 index 00000000..8cd20d31 --- /dev/null +++ b/docs/book/ahead-of-time-factories.md @@ -0,0 +1,105 @@ +# Ahead of Time Factories + +- 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. +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 [] +``` + +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 `` 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. + +## 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 `[].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: + +**after** +```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. diff --git a/mkdocs.yml b/mkdocs.yml index 35bad2f6..ea0ce027 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,7 @@ nav: - 'Plugin managers': plugin-managers.md - 'Configuration-based Abstract Factory': config-abstract-factory.md - 'Reflection-based Abstract Factory': reflection-abstract-factory.md + - 'Ahead of Time Factories': ahead-of-time-factories.md - 'Console Tools': console-tools.md - Cookbook: - 'Factories vs Abstract Factories': cookbook/factories-vs-abstract-factories.md From 595d69cead35643055e9326cf0abea503ee66da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:08:57 +0200 Subject: [PATCH 05/32] docs: re-order menu items so that there is a `Reflection-based Abstract Factory` category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index ea0ce027..7e405119 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,8 @@ nav: - 'Lazy services': lazy-services.md - 'Plugin managers': plugin-managers.md - 'Configuration-based Abstract Factory': config-abstract-factory.md - - 'Reflection-based Abstract Factory': reflection-abstract-factory.md + - 'Reflection-based Abstract Factory': + - 'Usage': reflection-abstract-factory.md - 'Ahead of Time Factories': ahead-of-time-factories.md - 'Console Tools': console-tools.md - Cookbook: From 5363f9ebc8ec28ef6695be6a8d49e82cada4cbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:12:59 +0200 Subject: [PATCH 06/32] docs: enhance documentation regarding CI pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/ahead-of-time-factories.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/book/ahead-of-time-factories.md b/docs/book/ahead-of-time-factories.md index 8cd20d31..cef78d94 100644 --- a/docs/book/ahead-of-time-factories.md +++ b/docs/book/ahead-of-time-factories.md @@ -23,6 +23,8 @@ When the CLI command has finished, there are all factories generated within the 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 `` 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. @@ -86,7 +88,6 @@ The CLI command to generate the factories expects a path to a file, which will b 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: -**after** ```php return [ 'service_manager' => [ From 124d5304d45188f51c5907ca49a762ed1198fa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 6 Apr 2023 00:22:58 +0200 Subject: [PATCH 07/32] docs: versionize documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/index.md | 1 - docs/book/{ => v3}/config-abstract-factory.md | 0 .../configuring-the-service-manager.md | 0 docs/book/{ => v3}/console-tools.md | 0 .../factories-vs-abstract-factories.md | 0 docs/book/{ => v3}/delegators.md | 0 docs/book/{ => v3}/index.html | 0 docs/book/v3/index.md | 1 + docs/book/{ => v3}/migration.md | 0 docs/book/{ => v3}/psr-11.md | 0 docs/book/{ => v3}/quick-start.md | 0 docs/book/{ => v4}/ahead-of-time-factories.md | 0 docs/book/v4/config-abstract-factory.md | 144 ++ .../v4/configuring-the-service-manager.md | 594 ++++++++ docs/book/v4/console-tools.md | 74 + .../factories-vs-abstract-factories.md | 98 ++ docs/book/v4/delegators.md | 209 +++ docs/book/v4/index.html | 10 + docs/book/v4/index.md | 1 + docs/book/{ => v4}/lazy-services.md | 0 docs/book/v4/migration.md | 1355 +++++++++++++++++ docs/book/{ => v4}/plugin-managers.md | 0 docs/book/v4/psr-11.md | 55 + docs/book/v4/quick-start.md | 67 + .../{ => v4}/reflection-abstract-factory.md | 0 25 files changed, 2608 insertions(+), 1 deletion(-) delete mode 120000 docs/book/index.md rename docs/book/{ => v3}/config-abstract-factory.md (100%) rename docs/book/{ => v3}/configuring-the-service-manager.md (100%) rename docs/book/{ => v3}/console-tools.md (100%) rename docs/book/{ => v3}/cookbook/factories-vs-abstract-factories.md (100%) rename docs/book/{ => v3}/delegators.md (100%) rename docs/book/{ => v3}/index.html (100%) create mode 120000 docs/book/v3/index.md rename docs/book/{ => v3}/migration.md (100%) rename docs/book/{ => v3}/psr-11.md (100%) rename docs/book/{ => v3}/quick-start.md (100%) rename docs/book/{ => v4}/ahead-of-time-factories.md (100%) create mode 100644 docs/book/v4/config-abstract-factory.md create mode 100644 docs/book/v4/configuring-the-service-manager.md create mode 100644 docs/book/v4/console-tools.md create mode 100644 docs/book/v4/cookbook/factories-vs-abstract-factories.md create mode 100644 docs/book/v4/delegators.md create mode 100644 docs/book/v4/index.html create mode 120000 docs/book/v4/index.md rename docs/book/{ => v4}/lazy-services.md (100%) create mode 100644 docs/book/v4/migration.md rename docs/book/{ => v4}/plugin-managers.md (100%) create mode 100644 docs/book/v4/psr-11.md create mode 100644 docs/book/v4/quick-start.md rename docs/book/{ => v4}/reflection-abstract-factory.md (100%) diff --git a/docs/book/index.md b/docs/book/index.md deleted file mode 120000 index fe840054..00000000 --- a/docs/book/index.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md \ No newline at end of file diff --git a/docs/book/config-abstract-factory.md b/docs/book/v3/config-abstract-factory.md similarity index 100% rename from docs/book/config-abstract-factory.md rename to docs/book/v3/config-abstract-factory.md diff --git a/docs/book/configuring-the-service-manager.md b/docs/book/v3/configuring-the-service-manager.md similarity index 100% rename from docs/book/configuring-the-service-manager.md rename to docs/book/v3/configuring-the-service-manager.md diff --git a/docs/book/console-tools.md b/docs/book/v3/console-tools.md similarity index 100% rename from docs/book/console-tools.md rename to docs/book/v3/console-tools.md diff --git a/docs/book/cookbook/factories-vs-abstract-factories.md b/docs/book/v3/cookbook/factories-vs-abstract-factories.md similarity index 100% rename from docs/book/cookbook/factories-vs-abstract-factories.md rename to docs/book/v3/cookbook/factories-vs-abstract-factories.md diff --git a/docs/book/delegators.md b/docs/book/v3/delegators.md similarity index 100% rename from docs/book/delegators.md rename to docs/book/v3/delegators.md diff --git a/docs/book/index.html b/docs/book/v3/index.html similarity index 100% rename from docs/book/index.html rename to docs/book/v3/index.html diff --git a/docs/book/v3/index.md b/docs/book/v3/index.md new file mode 120000 index 00000000..8a33348c --- /dev/null +++ b/docs/book/v3/index.md @@ -0,0 +1 @@ +../../../README.md \ No newline at end of file diff --git a/docs/book/migration.md b/docs/book/v3/migration.md similarity index 100% rename from docs/book/migration.md rename to docs/book/v3/migration.md diff --git a/docs/book/psr-11.md b/docs/book/v3/psr-11.md similarity index 100% rename from docs/book/psr-11.md rename to docs/book/v3/psr-11.md diff --git a/docs/book/quick-start.md b/docs/book/v3/quick-start.md similarity index 100% rename from docs/book/quick-start.md rename to docs/book/v3/quick-start.md diff --git a/docs/book/ahead-of-time-factories.md b/docs/book/v4/ahead-of-time-factories.md similarity index 100% rename from docs/book/ahead-of-time-factories.md rename to docs/book/v4/ahead-of-time-factories.md diff --git a/docs/book/v4/config-abstract-factory.md b/docs/book/v4/config-abstract-factory.md new file mode 100644 index 00000000..fd9c2dc4 --- /dev/null +++ b/docs/book/v4/config-abstract-factory.md @@ -0,0 +1,144 @@ +# Config Abstract Factory + +- Since 3.2.0 + +You can simplify the process of creating factories by registering +`Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory` with your service +manager instance. This allows you to define services using a configuration map, +rather than having to create separate factories for each of your services. + +## Enabling the ConfigAbstractFactory + +Enable the `ConfigAbstractFactory` in the same way that you would enable +any other abstract factory. + +Programmatically: + +```php +$serviceManager = new ServiceManager(); +$serviceManager->addAbstractFactory(new ConfigAbstractFactory()); +``` + +Or within configuration: + +```php +return [ + // laminas-mvc: + 'service_manager' => [ + 'abstract_factories' => [ + ConfigAbstractFactory::class, + ], + ], + + // mezzio or ConfigProvider consumers: + 'dependencies' => [ + 'abstract_factories' => [ + ConfigAbstractFactory::class, + ], + ], +]; +``` + +Like all abstract factories starting in version 3, you may also use the config +abstract factory as a mapped factory, registering it as a factory for a specific +class: + +```php +return [ + 'service_manager' => [ + 'factories' => [ + SomeCustomClass::class => ConfigAbstractFactory::class, + ], + ], +]; +``` + +## Configuration + +Configuration should be provided via the `config` service, which should return +an array or `ArrayObject`. `ConfigAbstractFactory` looks for a top-level key in +this service named after itself (i.e., `Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory`) +that is an array value. Each item in the array: + +- Should have a key representing the service name (typically the fully + qualified class name) +- Should have a value that is an array of each dependency, ordered using the + constructor argument order, and using service names registered with the + container. + +As an example: + +```php +use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; + +return [ + ConfigAbstractFactory::class => [ + MyInvokableClass::class => [], + MySimpleClass::class => [ + Logger::class, + ], + Logger::class => [ + Handler::class, + ], + ], +]; +``` + +The definition tells the service manager how this abstract factory should manage +dependencies in the classes defined. In the above example, `MySimpleClass` has a +single dependency on a `Logger` instance. The abstract factory will simply look +to fulfil that dependency by calling `get()` with that key on the container +passed to it. In this way, you can create the correct tree of +dependencies to successfully return any given service. + +In the above example, note that the abstract factory configuration does not +contain configuration for the `Handler` class. At first glance, this appears as +if it will fail; however, if `Handler` is configured directly with the container +already — for example, mapped to a custom factory — the service will +be created and used as a dependency. + +As another, more complete example, consider the following classes: + +```php +class UserMapper +{ + public function __construct(Adapter $db, Cache $cache) {} +} + +class Adapter +{ + public function __construct(array $config) {} +} + +class Cache +{ + public function __construct(CacheAdapter $cacheAdapter) {} +} + +class CacheAdapter +{ +} +``` + +In this case, we can define the configuration for these classes as follows: + +```php +// config/autoload/dependencies.php or anywhere that gets merged into global config +return [ + ConfigAbstractFactory::class => [ + CacheAdapter::class => [], // no dependencies + Cache::class => [ + CacheAdapter::class, // dependency on the CacheAdapter key defined above + ], + UserMapper::class => [ + Adapter::class, // will be called using normal factory defined below + Cache::class, // defined above and will be created using this abstract factory + ], + ], + 'service_manager' => [ + 'factories' => [ + Adapter::class => AdapterFactory::class, // normal factory not using above config + ], + ], +], +``` diff --git a/docs/book/v4/configuring-the-service-manager.md b/docs/book/v4/configuring-the-service-manager.md new file mode 100644 index 00000000..6e09e176 --- /dev/null +++ b/docs/book/v4/configuring-the-service-manager.md @@ -0,0 +1,594 @@ +# Configuring the service manager + +The Service Manager component can be configured by passing an associative array to the component's +constructor. The following keys are: + +- `services`: associative array that maps a key to a service instance. +- `invokables`: an associative array that maps a key to a constructor-less service; + i.e., for services that do not require arguments to the constructor. The key and + service name usually are the same; if they are not, the key is treated as an alias. +- `factories`: associative array that map a key to a factory name, or any callable. +- `abstract_factories`: a list of abstract factories classes. An abstract + factory is a factory that can potentially create any object, based on some + criterias. +- `delegators`: an associative array that maps service keys to lists of delegator factory keys, see the [delegators documentation](delegators.md) for more details. +- `aliases`: associative array that map a key to a service key (or another alias). +- `initializers`: a list of callable or initializers that are run whenever a service has been created. +- `lazy_services`: configuration for the lazy service proxy manager, and a class + map of service:class pairs that will act as lazy services; see the + [lazy services documentation](lazy-services.md) for more details. +- `shared`: associative array that maps a service name to a boolean, in order to + indicate to the service manager whether or not it should cache services it + creates via `get` method, independent of the `shared_by_default` setting. +- `shared_by_default`: boolean that indicates whether services created through + the `get` method should be cached. This is `true` by default. + +Here is an example of how you could configure a service manager: + +```php +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new ServiceManager([ + 'services' => [], + 'invokables' => [], + 'factories' => [], + 'abstract_factories' => [], + 'delegators' => [], + 'aliases' => [], + 'initializers' => [], + 'lazy_services' => [], + 'shared' => [], + 'shared_by_default' => true, +]); +``` + +## Factories + +A factory is any callable or any class that implements the interface +`Laminas\ServiceManager\Factory\FactoryInterface`. + +Service manager components provide a default factory that can be used to create +objects that do not have any dependencies: + +```php +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\ServiceManager; +use stdClass; + +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class, + MyObject::class => MyObjectFactory::class, + ], +]); +``` + +> For invokable classes we recommend using `Laminas\ServiceManager\Factory\InvokableFactory`, +> because ServiceManager will convert all `invokables` into `factories` using `InvokableFactory` internally. + +As said before, a factory can also be a callable, to create more complex objects: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\ServiceManager; +use stdClass; + +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class, + MyObject::class => function(ContainerInterface $container, $requestedName) { + $dependency = $container->get(stdClass::class); + return new MyObject($dependency); + }, + ], +]); +``` + +Each factory always receive a `ContainerInterface` argument (this is the base +interface that the `ServiceManager` implements), as well as the requested name +as the second argument. In this case, the `$requestedName` is `MyObject`. + +Alternatively, the above code can be replaced by a factory class instead of a +closure. This leads to more readable code. For instance: + +```php +// In MyObjectFactory.php file + +class MyObjectFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $dependency = $container->get(stdClass::class); + return new MyObject($dependency); + } +} + +// or without implementing the interface: +class MyObjectFactory +{ + public function __invoke(ContainerInterface $container, $requestedName) + { + $dependency = $container->get(Dependency::class); + return new MyObject($dependency); + } +} + +// When creating the service manager: +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class, + MyObject::class => MyObjectFactory::class + ] +]); +``` + +> For performance reasons, factories objects are not created until requested. +> In the above example, this means that the `MyObjectFactory` object won't be +> created until `MyObject` is requested. + +### Mapping multiple service to the same factory + +Unlike version 2 implementations of the component, in the version 3 +implementation, the `$requestedName` is guaranteed to be passed as the second +parameter of a factory. This is useful when you need to create multiple +services that are created exactly the same way, hence reducing the number of +needed factories. + +For instance, if two services share the same creation pattern, you could attach the same factory: + +```php +// In MyObjectFactory.php file + +class MyObjectFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $dependency = $container->get(stdClass::class); + return new $requestedName($dependency); + } +} + +// or without implementing the interface: +class MyObjectFactory +{ + public function __invoke(ContainerInterface $container, $requestedName) + { + $dependency = $container->get(Dependency::class); + return new $requestedName($dependency); + } +} + +// When creating the service manager: +$serviceManager = new ServiceManager([ + 'factories' => [ + MyObjectA::class => MyObjectFactory::class, + MyObjectB::class => MyObjectFactory::class + ] +]); +``` + +This pattern can often replace abstract factories, and is more performant: + +- Lookups for services do not need to query abstract factories; the service is + mapped explicitly. +- Once the factory is loaded for any object, it stays in memory for any other + service using the same factory. + +Using factories is recommended in most cases where abstract factories were used +in version 2. + +This feature *can* be abused, however: for instance, if you have dozens of +services that share the same creation, but which do not share any common +functionality, we recommend to create separate factories. + +## Abstract factories + +An abstract factory is a specialized factory that can be used to create any +service, if it has the capability to do so. An abstract factory is often useful +when you do not know in advance the name of the service (e.g. if the service +name is generated dynamically at runtime), but know that the services share a +common creation pattern. + +An abstract factory must be registered inside the service manager, and is +checked if no factory can create an object. Each abstract factory must +implement `Laminas\ServiceManager\Factory\AbstractFactoryInterface`: + +```php +// In MyAbstractFactory.php: + +class MyAbstractFactory implements AbstractFactoryInterface +{ + public function canCreate(ContainerInterface $container, $requestedName) + { + return in_array('Traversable', class_implements($requestedName), true); + } + + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + return $requestedName(); + } +} + +// When creating the service manager: +$serviceManager = new ServiceManager([ + 'abstract_factories' => [ + new MyAbstractFactory() // You could also pass a class name: MyAbstractFactory::class + ] +]); + +// When fetching an object: +$object = $serviceManager->get(A::class); +``` + +Here is what will happen: + +1. The service manager will check if it contains a factory mapped to the + `A::class` service. +2. Because none is found, it will process each abstract factory, in the order + in which they were registered. +3. It will call the `canCreate()` method, passing the service manager instance and + the name of the requested object. The method can use any logic whatsoever to + determine if it can create the service (such as checking its name, checking + for a required dependency in the passed container, checking if a class + implements a given interface, etc.). +4. If `canCreate()` returns `true`, it will call the `__invoke` method to + create the object. Otherwise, it will continue iterating the abstract + factories, until one matches, or the queue is exhausted. + +### Best practices + +While convenient, we recommend you to limit the number of abstract factories. +Because the service manager needs to iterate through all registered abstract +factories to resolve services, it can be costly when multiple abstract +factories are present. + +Often, mapping the same factory to multiple services can solve the issue more +efficiently (as described in the `Factories` section). + +## Aliases + +An *alias* provides an alternative name for a registered service. + +An alias can also be mapped to another alias (it will be resolved recursively). +For instance: + +```php +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\ServiceManager; +use stdClass; + +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class + ], + + 'aliases' => [ + 'A' => stdClass::class, + 'B' => 'A' + ] +]); + +$object = $serviceManager->get('B'); +``` + +In this example, asking `B` will be resolved to `A`, which will be itself +resolved to `stdClass::class`, which will finally be constructed using the +provided factory. + +### Best practices + +We recommend you minimal use of aliases, and instead using the `::class` +language construct to map using a FQCN (Fully-Qualified-Class-Name). This +provides both better discoverability within your code, and allows simpler +refactoring, as most modern IDEs can refactor class names specified using the +`::class` keyword. + +## Initializers + +An initializer is any callable or any class that implements the interface +`Laminas\ServiceManager\Initializer\InitializerInterface`. Initializers are +executed for each service the first time they are created, and can be used to +inject additional dependencies. + +For instance, if we'd want to automatically inject the dependency +`EventManager::class` in all objects that implement the interface +`EventManagerAwareInterface`, we could create the following initializer: + +```php +use Interop\Container\ContainerInterface; +use stdClass; +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new ServiceManager([ + 'initializers' => [ + function(ContainerInterface $container, $instance) { + if (! $instance instanceof EventManagerAwareInterface) { + return; + } + $instance->setEventManager($container->get(EventManager::class)); + } + ] +]); +``` + +Alternately, you can create a class that implements +`Laminas\ServiceManager\Initializer\InitializerInterface`, and pass it to the +`initializers` array: + +```php +// In MyInitializer.php + +class MyInitializer implements InitializerInterface +{ + public function __invoke(ContainerInterface $container, $instance) + { + if (! $instance instanceof EventManagerAwareInterface) { + return; + } + $instance->setEventManager($container->get(EventManager::class)); + } +} + +// When creating the service manager: + +use Interop\Container\ContainerInterface; +use stdClass; +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new ServiceManager([ + 'initializers' => [ + new MyInitializer() // You could also use MyInitializer::class + ] +]); +``` + +> Note that initializers are automatically created when the service manager is +> initialized, even if you pass a class name. + +### Best practices + +While convenient, initializer usage is also problematic. They are provided +primarily for backwards compatibility, but we highly discourage their usage. + +The primary issues with initializers are: + +- They lead to fragile code. Because the dependency is not injected directly in + the constructor, it means that the object may be in an "incomplete state". If + for any reason the initializer is not run (if it was not correctly registered + for instance), bugs ranging from the subtle to fatal can be introduced. + + Instead, we encourage you to inject all necessary dependencies via + the constructor, using factories. If some dependencies use setter or interface + injection, use delegator factories. + + If a given service has too many dependencies, then it may be a sign that you + need to split this service into smaller, more focused services. + +- They are slow: an initializer is run for EVERY instance you create through + the service manager. If you have ten initializers or more, this can quickly + add up! + +## Shared + +By default, a service created is shared. This means that calling the `get()` +method twice for a given service will return exactly the same service. This is +typically what you want, as it can save a lot of memory and increase +performance: + +```php +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class + ] +]); + +$object1 = $serviceManager->get(stdClass::class); +$object2 = $serviceManager->get(stdClass::class); + +var_dump($object1 === $object2); // prints "true" +``` + +However, occasionally you may require discrete instances of a service. To +enable this, you can use the `shared` key, providing a boolean false value for +your service, as shown below: + +```php +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class + ], + 'shared' => [ + stdClass::class => false + ] +]); + +$object1 = $serviceManager->get(stdClass::class); +$object2 = $serviceManager->get(stdClass::class); + +var_dump($object1 === $object2); // prints "false" +``` + +Alternately, you can use the `build()` method instead of the `get()` method. +The `build()` method works exactly the same as the `get` method, but never +caches the service created, nor uses a previously cached instance for the +service. + +```php +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class + ] +]); + +$object1 = $serviceManager->build(stdClass::class); +$object2 = $serviceManager->build(stdClass::class); + +var_dump($object1 === $object2); // prints "false" +``` + +Finally, you could also decide to disable caching by default (even when calling +the `get()` method), by setting the `shared_by_default` option to false: + +```php +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class + ], + 'shared_by_default' => false, +]); + +$object1 = $serviceManager->get(stdClass::class); +$object2 = $serviceManager->get(stdClass::class); + +var_dump($object1 === $object2); // prints "false" +``` + +## Passing config to a factory/delegator + +So far, we have covered examples where services are created through factories +(or abstract factories). The factory is able to create the object itself. + +Occasionally you may need to pass additional options that act as a "context". +For instance, we could have a `StringLengthValidator` service registered. +However, this validator can have multiple options, such as `min` and `max`. +Because this is dependent on the caller context (or might even be retrieved +from a database, for instance), the factory cannot know what options to give +when constructing the validator. + +To solve this issue, the service manager offers a `build()` method. It works +similarly to the `get()` method, with two main differences: + +- Services created with the `build()` method are **never cached**, nor pulled + from previously cached instances for that service. +- `build()` accepts an optional secondary parameter, an array of options. + +Those options are transferred to all factories, abstract factories, and delegators. +For instance: + +```php +// In StringLengthValidatorFactory.php + +class StringLengthValidatorFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = []) + { + return new StringLengthValidator($options); + } +} + +// When creating the service manager: +$serviceManager = new ServiceManager([ + 'factories' => [ + StringLengthValidator::class => StringLengthValidatorFactory::class + ] +]); + +// When creating the objects: + +$validator1 = $serviceManager->build(StringLengthValidator::class, ['min' => 5]); +$validator2 = $serviceManager->build(StringLengthValidator::class, ['min' => 15]); +``` + +In our previous example, because the `StringLengthValidator` does not have any +other dependencies other than the `$options`, we could remove the factory, and +simply map it to the built-in `InvokableFactory` factory: + +```php +// When creating the service manager: +$serviceManager = new ServiceManager([ + 'factories' => [ + StringLengthValidator::class => InvokableFactory::class + ] +]); + +// When creating the objects: + +$validator1 = $serviceManager->build(StringLengthValidator::class, ['min' => 5]); +$validator2 = $serviceManager->build(StringLengthValidator::class, ['min' => 15]); +``` + +This works because the `InvokableFactory` will automatically pass the options +(if any) to the constructor of the created object. + +## Altering a service manager's config + +Assuming that you have not called `$container->setAllowOverride(false)`, you can, +at any time, configure the service manager with new services using any of the +following methods: + +- `configure()`, which accepts the same configuration array as the constructor. +- `setAlias($alias, $target)` +- `setInvokableClass($name, $class = null)`; if no `$class` is passed, the + assumption is that `$name` is the class name. +- `setFactory($name, $factory)`, where `$factory` can be either a callable + factory or the name of a factory class to use. +- `mapLazyService($name, $class = null)`, to map the service name `$name` to + `$class`; if the latter is not provided, `$name` is used for both sides of + the map. +- `addAbstractFactory($factory)`, where `$factory` can be either a + `Laminas\ServiceManager\Factory\AbstractFactoryInterface` instance or the name + of a class implementing the interface. +- `addDelegator($name, $factory)`, where `$factory` can be either a callable + delegator factory, or the name of a delegator factory class to use. +- `addInitializer($initializer)`, where `$initializer` can be either a callable + initializer, or the name of an initializer class to use. +- `setService($name, $instance)` +- `setShared($name, $shared)`, where `$shared` is a boolean flag indicating + whether or not the named service should be shared. + +As examples: + +```php +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class; + ] +]); + +$serviceManager->configure([ + 'factories' => [ + DateTime::class => InvokableFactory::class + ] +]); + +var_dump($newServiceManager->has(DateTime::class)); // prints true + +// Create an alias from 'Date' to 'DateTime' +$serviceManager->setAlias('Date', DateTime::class); + +// Set a factory for the 'Time' service +$serviceManager->setFactory('Time', function ($container) { + return $container->get(DateTime::class); +}); + +// Map a lazy service named 'localtime' to the class DateTime. +$serviceManager->mapLazyService('localtime', DateTime::class); + +// Add an abstract factory +$serviceManager->addAbstractFactory(new CustomAbstractFactory()); + +// Add a delegator factory for the DateTime service +$serviceManager->addDelegator(DateTime::class, function ($container, $name, $callback) { + $dateTime = $callback(); + $dateTime->setTimezone(new DateTimezone('UTC')); + return $dateTime; +}); + +// Add an initializer +// Note: don't do this. Use delegator factories instead. +$serviceManager->addInitializer(function ($service, $instance) { + if (! $instance instanceof DateTime) { + return; + } + $instance->setTimezone(new DateTimezone('America/Chicago')); +}) + +// Explicitly map a service name to an instance. +$serviceManager->setService('foo', new stdClass); + +// Mark the DateTime service as NOT being shared. +$serviceManager->setShared(DateTime::class, false); +``` diff --git a/docs/book/v4/console-tools.md b/docs/book/v4/console-tools.md new file mode 100644 index 00000000..d54de33a --- /dev/null +++ b/docs/book/v4/console-tools.md @@ -0,0 +1,74 @@ +# Console Tools + +Starting in 3.2.0, laminas-servicemanager began shipping with console tools. This +document details each. + +## generate-deps-for-config-factory + +```bash +$ ./vendor/bin/generate-deps-for-config-factory +Usage: + + generate-deps-for-config-factory [-h|--help|help] [-i|--ignore-unresolved] + +Arguments: + + -h|--help|help This usage message + -i|--ignore-unresolved Ignore classes with unresolved direct dependencies. + Path to a config file for which to generate + configuration. If the file does not exist, it will + be created. If it does exist, it must return an + array, and the file will be updated with new + configuration. + Name of the class to reflect and for which to + generate dependency configuration. + + +Reads the provided configuration file (creating it if it does not exist), +and injects it with ConfigAbstractFactory dependency configuration for +the provided class name, writing the changes back to the file. +``` + +This utility will generate dependency configuration for the named class for use +with the [ConfigAbstractFactory](config-abstract-factory.md). When doing so, it +will read the named configuration file (creating it if it does not exist), and +merge any configuration it generates with the return values of that file, +writing the changes back to the original file. + +Since 3.2.1, the tool also supports the `-i` or `--ignore-unresolved` flag. +Use these flags when you have typehints to classes that cannot be resolved. +When you omit the flag, such classes will cause the tool to fail with an +exception message. By adding the flag, you can have it continue and produce +configuration. This option is particularly useful when typehints are on +interfaces or resolve to services served by other abstract factories. + +## generate-factory-for-class + +```bash +$ ./vendor/bin/generate-factory-for-class + +Usage: + + ./bin/generate-factory-for-class [-h|--help|help] + +Arguments: + + -h|--help|help This usage message + Name of the class to reflect and for which to generate + a factory. + +Generates to STDOUT a factory for creating the specified class; this may then +be added to your application, and configured as a factory for the class. +``` + +This utility generates a factory class for the given class, based on the +typehints in its constructor. The factory is emitted to STDOUT, and may be piped +to a file if desired: + +```bash +$ ./vendor/bin/generate-factory-for-class \ +> "Application\\Model\\AlbumModel" > ./module/Application/src/Model/AlbumModelFactory.php +``` + +The class generated implements `Laminas\ServiceManager\Factory\FactoryInterface`, +and is generated within the same namespace as the originating class. diff --git a/docs/book/v4/cookbook/factories-vs-abstract-factories.md b/docs/book/v4/cookbook/factories-vs-abstract-factories.md new file mode 100644 index 00000000..986747d5 --- /dev/null +++ b/docs/book/v4/cookbook/factories-vs-abstract-factories.md @@ -0,0 +1,98 @@ +# When To Use Factories vs Abstract Factories + +Starting with version 3, `Laminas\ServiceManager\Factory\AbstractFactoryInterface` +extends `Laminas\ServiceManager\Factory\FactoryInterface`, meaning they may be used +as either an abstract factory, or mapped to a specific service name as its +factory. + +As an example: + +```php +return [ + 'factories' => [ + SomeService::class => AnAbstractFactory::class, + ], +]; +``` + +Why would you choose one approach over the other? + +## Comparisons + +Approach | Pros | Cons +---------------- | -------------- | ---- +Abstract factory | One-time setup | Performance; discovery of code responsible for creating instance +Factory | Performance; explicit mapping to factory responsible | Additional (duplicate) setup + +Essentially, it comes down to *convenience* versus *explicitness* and/or +*performance*. + +## Convenience + +Writing a factory per service is time consuming, and, particularly in early +stages of an application, can distract from the actual business of writing the +classes and implementations; in addition, since requirements are often changing +regularly, this boiler-plate code can be a nuisance. + +In such situations, one or more abstract factories — such as the +[ConfigAbstractFactory](../config-abstract-factory.md), the +[ReflectionBasedAbstractFactory](../reflection-abstract-factory.md), or the +[laminas-mvc LazyControllerAbstractFactory](https://docs.laminas.dev/laminas-mvc/cookbook/automating-controller-factories/) +— that can handle the bulk of your needs are often worthwhile, saving you +time and effort as you code. + +## Explicitness + +The drawback of abstract factories is that lookups by the service manager take +longer, and increase based on the number of abstract factories in the system. +The service manager is optimized to locate *factories*, as it can do an +immediate hash table lookup; abstract factories involve: + +- Looping through each abstract factory + - invoking its method for service location + - if the service is located, using the factory + +This means, internally: + +- a hash table lookup (for the abstract factory) +- invocation of 1:N methods for discovery + - which may contain additional lookups and/or retrievals in the container +- invocation of a factory method (assuming successful lookup) + +As such, having an explicit map can aid performance dramatically. + +Additionally, having an explicit map can aid in understanding what class is +responsible for initializing a given service. Without an explicit map, you need +to identify all possible abstract factories, and determine which one is capable +of handling the specific service; in some cases, multiple factories might be +able to, which means you additionally need to know the *order* in which they +will be queried. + +The primary drawback is that you also end up with potentially duplicate +information in your configuration: + +- Multiple services mapped to the same factory. +- In cases such as the `ConfigAbstractFactory`, additional configuration + detailing how to create the service. + +## Tradeoffs + +What it comes down to is which development aspects your organization or project +favor. Hopefully the above arguments detail what tradeoffs occur, so you may +make an appropriate choice. + +## Tooling + +Starting with 3.2.0, we began offering a variety of [console tools](../console-tools.md) +to assist you in generating both dependency configuration and factories. Use +these to help your code evolve. An expected workflow in your application +development evolution is: + +- Usage of the `ReflectionBasedAbstractFactory` as a "catch-all", so that you + do not need to do any factory/dependency configuration immediately. +- Usage of the `ConfigAbstractFactory`, mapped to services, once dependencies + have settled, to disambiguate dependencies, or to list custom services + returning scalar or array values. +- Finally, usage of the `generate-factory-for-class` vendor binary to generate + actual factory classes for your production-ready code, providing the best + performance. diff --git a/docs/book/v4/delegators.md b/docs/book/v4/delegators.md new file mode 100644 index 00000000..a487bef7 --- /dev/null +++ b/docs/book/v4/delegators.md @@ -0,0 +1,209 @@ +# Delegators + +`Laminas\ServiceManager` can instantiate [delegators](http://en.wikipedia.org/wiki/Delegation_pattern) +of requested services, decorating them as specified in a delegate factory +implementing the [delegator factory interface](https://github.com/laminas/laminas-servicemanager/tree/master/src/Factory/DelegatorFactoryInterface.php). + +The delegate pattern is useful in cases when you want to wrap a real service in +a [decorator](http://en.wikipedia.org/wiki/Decorator_pattern), or generally +intercept actions being performed on the delegate in an +[AOP](http://en.wikipedia.org/wiki/Aspect-oriented_programming) fashioned way. + +## Delegator factory signature + +A delegator factory has the following signature: + +```php +use Interop\Container\ContainerInterface; + +public function __invoke( + ContainerInterface $container, + $name, + callable $callback, + array $options = null +); +``` + +The parameters passed to the delegator factory are the following: + +- `$container` is the service locator that is used while creating the delegator + for the requested service. +- `$name` is the name of the service being requested. +- `$callback` is a [callable](http://www.php.net/manual/en/language.types.callable.php) that is + responsible for instantiating the delegated service (the real service instance). +- `$options` is an array of options to use when creating the instance; these are + typically used only during `build()` operations. + +## A Delegator factory use case + +A typical use case for delegators is to handle logic before or after a method is +called. + +In the following example, an event is being triggered before `Buzzer::buzz()` is +called and some output text is prepended. + +The delegated object `Buzzer` (original object) is defined as following: + +```php +class Buzzer +{ + public function buzz() + { + return 'Buzz!'; + } +} +``` + +The delegator class `BuzzerDelegator` has the following structure: + +```php +use Laminas\EventManager\EventManagerInterface; + +class BuzzerDelegator extends Buzzer +{ + protected $realBuzzer; + protected $eventManager; + + public function __construct(Buzzer $realBuzzer, EventManagerInterface $eventManager) + { + $this->realBuzzer = $realBuzzer; + $this->eventManager = $eventManager; + } + + public function buzz() + { + $this->eventManager->trigger('buzz', $this); + + return $this->realBuzzer->buzz(); + } +} +``` + +To use the `BuzzerDelegator`, you can run the following code: + +```php +$wrappedBuzzer = new Buzzer(); +$eventManager = new Laminas\EventManager\EventManager(); + +$eventManager->attach('buzz', function () { echo "Stare at the art!\n"; }); + +$buzzer = new BuzzerDelegator($wrappedBuzzer, $eventManager); + +echo $buzzer->buzz(); // "Stare at the art!\nBuzz!" +``` + +This logic is fairly simple as long as you have access to the instantiation +logic of the `$wrappedBuzzer` object. + +You may not always be able to define how `$wrappedBuzzer` is created, since a +factory for it may be defined by some code to which you don't have access, or +which you cannot modify without introducing further complexity. + +Delegator factories solve this specific problem by allowing you to wrap, +decorate or modify any existing service. + +A simple delegator factory for the `buzzer` service can be implemented as +following: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; + +class BuzzerDelegatorFactory implements DelegatorFactoryInterface +{ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null) + { + $realBuzzer = call_user_func($callback); + $eventManager = $container->get('EventManager'); + + $eventManager->attach('buzz', function () { echo "Stare at the art!\n"; }); + + return new BuzzerDelegator($realBuzzer, $eventManager); + } +} +``` + +You can then instruct the service manager to handle the service `buzzer` as a +delegate: + +```php +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new Laminas\ServiceManager\ServiceManager([ + 'factories' => [ + Buzzer::class => InvokableFactory::class, + ], + 'delegators' => [ + Buzzer::class => [ + BuzzerDelegatorFactory::class, + ], + ], +]); + +// now, when fetching Buzzer, we get a BuzzerDelegator instead +$buzzer = $serviceManager->get(Buzzer::class); + +$buzzer->buzz(); // "Stare at the art!\nBuzz!" +``` + +You can specify multiple delegators for a service. Each will add one decorator +around the instantiation logic of that particular service. + +This latter point is the primary use case for delegators: *decorating the +instantiation logic for a service*. + +## Delegator Factories and Service Aliases + +In typical [service manager configurations](./configuring-the-service-manager.md) you have the opportunity to alias services. The following configuration would enable you to retrieve a `Buzzer` instance by its concrete implementation name and by the name of an interface that it implements, in this case, `BuzzerInterface`. + +```php +$serviceManager = new Laminas\ServiceManager\ServiceManager([ + 'factories' => [ + Buzzer::class => Laminas\ServiceManager\Factory\InvokableFactory::class, + ], + 'aliases' => [ + BuzzerInterface::class => Buzzer::class, + ], +]); +``` + +Currently, a delegator factory that targets an alias will not execute. Delegators must be configured using the resolved name of the service. + +For example, given the following configuration, **no delegation would occur**: + +```php +$serviceManager = new Laminas\ServiceManager\ServiceManager([ + 'factories' => [ + Buzzer::class => Laminas\ServiceManager\Factory\InvokableFactory::class, + ], + 'aliases' => [ + BuzzerInterface::class => Buzzer::class, + ], + 'delegators' => [ + BuzzerInterface::class => [ + BuzzerDelegatorFactory::class, // will not be executed + ], + ], +]); +``` + +In order for delegation to occur, the above configuration would need to be modified to target the resolved service name: + +```php +$serviceManager = new Laminas\ServiceManager\ServiceManager([ + 'factories' => [ + Buzzer::class => Laminas\ServiceManager\Factory\InvokableFactory::class, + ], + 'aliases' => [ + BuzzerInterface::class => Buzzer::class, + ], + 'delegators' => [ + Buzzer::class => [ + BuzzerDelegatorFactory::class, // will now execute as expected + ], + ], +]); +``` + +Retrieving the `Buzzer` using its resolved name "`Buzzer::class`" or its alias "`BuzzerInterface::class`" will now both yield delegated instances. diff --git a/docs/book/v4/index.html b/docs/book/v4/index.html new file mode 100644 index 00000000..5c00f74d --- /dev/null +++ b/docs/book/v4/index.html @@ -0,0 +1,10 @@ +
+
+

laminas-servicemanager

+ +

Factory-Driven Dependency Injection Container

+ +
$ composer require laminas/laminas-servicemanager
+
+
+ diff --git a/docs/book/v4/index.md b/docs/book/v4/index.md new file mode 120000 index 00000000..8a33348c --- /dev/null +++ b/docs/book/v4/index.md @@ -0,0 +1 @@ +../../../README.md \ No newline at end of file diff --git a/docs/book/lazy-services.md b/docs/book/v4/lazy-services.md similarity index 100% rename from docs/book/lazy-services.md rename to docs/book/v4/lazy-services.md diff --git a/docs/book/v4/migration.md b/docs/book/v4/migration.md new file mode 100644 index 00000000..37950abf --- /dev/null +++ b/docs/book/v4/migration.md @@ -0,0 +1,1355 @@ +# Migration Guide + +The Service Manager was first introduced for Laminas.0.0. Its API +remained the same throughout that version. + +Version 3 is the first new major release of the Service Manager, and contains a +number of backwards compatibility breaks. These were introduced to provide +better performance and stability. + +## Case Sensitivity and Normalization + +v2 normalized service names as follows: + +- It stripped non alphanumeric characters. +- It lowercased the resulting string. + +This was done to help prevent typographical errors from creating configuration +errors. However, it also presented a large performance hit, and led to some +unexpected behaviors. + +**In v3, service names are case sensitive, and are not normalized in any way.** + +As such, you *must* refer to services using the same case in which they were +registered. + +## Configuration + +A number of changes have been made to configuration of service and plugin +managers: + +- Minor changes in configuration arrays may impact your usage. +- `ConfigInterface` implementations and consumers will need updating. + +### Configuration arrays + +Configuration for v2 consisted of the following: + +```php +[ + 'services' => [ + // service name => instance pairs + ], + 'aliases' => [ + // alias => service name pairs + ], + 'invokables' => [ + // service name => class name pairs + ], + 'factories' => [ + // service name => factory pairs + ], + 'abstract_factories' => [ + // abstract factories + ], + 'initializers' => [ + // initializers + ], + 'delegators' => [ + // service name => [ delegator factories ] + ], + 'shared' => [ + // service name => boolean + ], + 'share_by_default' => boolean, +] +``` + +In v3, the configuration remains the same, with the following additions: + +```php +[ + 'lazy_services' => [ + // The class_map is required if using lazy services: + 'class_map' => [ + // service name => class name pairs + ], + // The following are optional: + 'proxies_namespace' => 'Alternate namespace to use for generated proxy classes', + 'proxies_target_dir' => 'path in which to write generated proxy classes', + 'write_proxy_files' => true, // boolean; false by default + ], +] +``` + +The main change is the addition of integrated lazy service configuration is now +integrated. + +### ConfigInterface + +The principal change to the `ConfigInterface` is the addition of the +`toArray()` method. This method is intended to return a configuration array in +the format listed above, for passing to either the constructor or the +`configure()` method of the `ServiceManager`.. + +### Config class + +`Laminas\ServiceManager\Config` has been updated to follow the changes to the +`ConfigInterface` and `ServiceManager`. This essentially means that it removes +the various getter methods, and adds the `toArray()` method. + +## Invokables + +*Invokables no longer exist,* at least, not identically to how they existed in +Laminas. + +Internally, `ServiceManager` now does the following for `invokables` entries: + +- If the name and value match, it creates a `factories` entry mapping the + service name to `Laminas\ServiceManager\Factory\InvokableFactory`. +- If the name and value *do not* match, it creates an `aliases` entry mapping the + service name to the class name, *and* a `factories` entry mapping the class + name to `Laminas\ServiceManager\Factory\InvokableFactory`. + +This means that you can use your existing `invokables` configuration from +version 2 in version 3. However, we recommend starting to update your +configuration to remove `invokables` entries in favor of factories (and aliases, +if needed). + +> #### Invokables and plugin managers +> +> If you are creating a plugin manager and in-lining invokables into the class +> definition, you will need to make some changes. +> +> `$invokableClasses` will need to become `$factories` entries, and you will +> potentially need to add `$aliases` entries. +> +> As an example, consider the following, from laminas-math v2.x: +> +> ```php +> class AdapterPluginManager extends AbstractPluginManager +> { +> protected $invokableClasses = [ +> 'bcmath' => Adapter\Bcmath::class, +> 'gmp' => Adapter\Gmp::class, +> ]; +> } +> ``` +> +> Because we no longer define an `$invokableClasses` property, for v3.x, this +> now becomes: +> +> ```php +> use Laminas\ServiceManager\Factory\InvokableFactory; +> +> class AdapterPluginManager extends AbstractPluginManager +> { +> protected $aliases = [ +> 'bcmath' => Adapter\Bcmath::class, +> 'gmp' => Adapter\Gmp::class, +> ]; +> +> protected $factories = [ +> Adapter\BcMath::class => InvokableFactory::class, +> Adapter\Gmp::class => InvokableFactory::class, +> ]; +> } +> ``` + +## Lazy Services + +In v2, if you wanted to create a lazy service, you needed to take the following +steps: + +- Ensure you have a `config` service, with a `lazy_services` key that contained + the configuration necessary for the `LazyServiceFactory`. +- Assign the `LazyServiceFactoryFactory` as a factory for the + `LazyServiceFactory` +- Assign the `LazyServiceFactory` as a delegator factory for your service. + +As an example: + +```php +use Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory; + +$config = [ + 'lazy_services' => [ + 'class_map' => [ + 'MyClass' => 'MyClass', + ], + 'proxies_namespace' => 'TestAssetProxy', + 'proxies_target_dir' => 'data/proxies/', + 'write_proxy_files' => true, + ], +]; + +return [ + 'services' => [ + 'config' => $config, + ], + 'invokables' => [ + 'MyClass' => 'MyClass', + ], + 'factories' => [ + 'LazyServiceFactory' => LazyServiceFactoryFactory::class, + ], + 'delegators' => [ + 'MyClass' => [ + 'LazyServiceFactory', + ], + ], +]; +``` + +This was done in part because lazy services were introduced later in the v2 +cycle, and not fully integrated in order to retain the API. + +In order to reduce the number of dependencies and steps necessary to configure +lazy services, the following changes were made for v3: + +- Lazy service configuration can now be passed directly to the service manager; + it is no longer dependent on a `config` service. +- The ServiceManager itself is now responsible for creating the + `LazyServiceFactory` delegator factory, based on the configuration present. + +The above example becomes the following in v3: + +```php +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\Proxy\LazyServiceFactory; + +return [ + 'factories' => [ + 'MyClass' => InvokableFactory::class, + ], + 'delegators' => [ + 'MyClass' => [ + LazyServiceFactory::class, + ], + ], + 'lazy_services' => [ + 'class_map' => [ + 'MyClass' => 'MyClass', + ], + 'proxies_namespace' => 'TestAssetProxy', + 'proxies_target_dir' => 'data/proxies/', + 'write_proxy_files' => true, + ], +]; +``` + +Additionally, assuming you have configured lazy services initially with the +proxy namespace, target directory, etc., you can map lazy services using the new +method `mapLazyService($name, $class)`: + +```php +$container->mapLazyService('MyClass', 'MyClass'); +// or, more simply: +$container->mapLazyService('MyClass'); +``` + +## ServiceLocatorInterface Changes + +The `ServiceLocatorInterface` now extends the +[container-interop](https://github.com/container-interop/container-interop) +interface `ContainerInterface`, which defines the same `get()` and `has()` +methods as were previously defined. + +Additionally, it adds a new method: + +```php +public function build($name, array $options = null) +``` + +This method is defined to *always* return a *new* instance of the requested +service, and to allow using the provided `$options` when creating the instance. + +## ServiceManager API Changes + +`Laminas\ServiceManager\ServiceManager` remains the primary interface with which +developers will interact. It has the following changes in v3: + +- It adds a new method, `configure()`, which allows configuring all instance + generation capabilities (aliases, factories, abstract factories, etc.) at + once. +- Peering capabilities were removed. +- Exceptions are *always* thrown when service instance creation fails or + produces an error; you can no longer disable this. +- Configuration no longer requires a `Laminas\ServiceManager\Config` instance. + `Config` can be used, but is not needed. +- It adds a new method, `build()`, for creating discrete service instances. + +### Methods Removed + +*The following methods are removed* in v3: + +- `setShareByDefault()`/`shareByDefault()`; this can be passed during + instantiation or via `configure()`. +- `setThrowExceptionInCreate()`/`getThrowExceptionInCreate()`; exceptions are + *always* thrown when errors are encountered during service instance creation. +- `setRetrieveFromPeeringManagerFirst()`/`retrieveFromPeeringManagerFirst()`; + peering is no longer supported. + +### Constructor + +The constructor now accepts an array of service configuration, not a +`Laminas\ServiceManager\Config` instance. + +### Use `build()` for discrete instances + +The new method `build()` acts as a factory method for configured services, and +will *always* return a new instance, never a shared one. + +Additionally, it provides factory capabilities; you may pass an additional, +optional argument, `$options`, which should be an array of additional options a +factory may use to create a new instance. This is primarily of interest when +creating plugin managers (more on plugin managers below), which may pass that +information on in order to create discrete plugin instances with specific state. + +As examples: + +```php +use Laminas\Validator\Between; + +$between = $container->build(Between::class, [ + 'min' => 5, + 'max' => 10, + 'inclusive' => true, +]); + +$alsoBetween = $container->build(Between::class, [ + 'min' => 0, + 'max' => 100, + 'inclusive' => false, +]); +``` + +The above two validators would be different instances, with their own +configuration. + +## Factories + +Internally, the `ServiceManager` now only uses the new factory interfaces +defined in the `Laminas\ServiceManager\Factory` namespace. These *replace* the +interfaces defined in version 2, and define completely new signatures. + +For migration purposes, all original interfaces were retained, and now inherit +from the new interfaces. This provides a migration path; you can add the methods +defined in the new interfaces to your existing factories targeting v2, and +safely upgrade. (Typically, you will then have the version 2 methods proxy to +those defined in version 3.) + +### Interfaces and relations to version 2 + +| Version 2 Interface | Version 3 Interface | +| :-------------------------------------------------------: | :-------------------------------------------------------: | +| `Laminas\ServiceManager\AbstractFactoryInterface` | `Laminas\ServiceManager\Factory\AbstractFactoryInterface` | +| `Laminas\ServiceManager\DelegatorFactoryInterface` | `Laminas\ServiceManager\Factory\DelegatorFactoryInterface` | +| `Laminas\ServiceManager\FactoryInterface` | `Laminas\ServiceManager\Factory\FactoryInterface` | + +The version 2 interfaces now extend those in version 3, but are marked +**deprecated**. You can continue to use them, but will be required to update +your code to use the new interfaces in the future. + +### AbstractFactoryInterface + +The previous signature of the `AbstractFactoryInterface` was: + +```php +interface AbstractFactoryInterface +{ + /** + * Determine if we can create a service with name + * + * @param ServiceLocatorInterface $serviceLocator + * @param $name + * @param $requestedName + * @return bool + */ + public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); + + /** + * Create service with name + * + * @param ServiceLocatorInterface $serviceLocator + * @param $name + * @param $requestedName + * @return mixed + */ + public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); +} +``` + +The new signature is: + +```php +interface AbstractFactoryInterface extends FactoryInterface +{ + /** + * Does the factory have a way to create an instance for the service? + * + * @param ContainerInterface $container + * @param string $requestedName + * @return bool + */ + public function canCreate(ContainerInterface $container, $requestedName); +} +``` + +Note that it now *extends* the `FactoryInterface` (detailed below), and thus the +factory logic has the same signature. + +In v2, the abstract factory defined the method `canCreateServiceWithName()`; in +v3, this is renamed to `canCreate()`, and the method also now receives only two +arguments, the container and the requested service name. + +To prepare your version 2 implementation to work upon upgrade to version 3: + +- Add the methods `canCreate()` and `__invoke()` as defined in version 3. +- Modify your existing `canCreateServiceWithName()` method to proxy to + `canCreate()` +- Modify your existing `createServiceWithName()` method to proxy to + `__invoke()` + +As an example, given the following implementation from version 2: + +```php +use Laminas\ServiceManager\AbstractFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class LenientAbstractFactory implements AbstractFactoryInterface +{ + public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + return class_exists($requestedName); + } + + public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + return new $requestedName(); + } +} +``` + +To update this for version 3 compatibility, you will add the methods +`canCreate()` and `__invoke()`, move the code from the existing methods into +them, and update the existing methods to proxy to the new methods: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\AbstractFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class LenientAbstractFactory implements AbstractFactoryInterface +{ + public function canCreate(ContainerInterface $container, $requestedName) + { + return class_exists($requestedName); + } + + public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + return $this->canCreate($services, $requestedName); + } + + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + return new $requestedName(); + } + + public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) + { + return $this($services, $requestedName); + } +} +``` + +After you have upgraded to version 3, you can take the following steps to remove +the migration artifacts: + +- Update your class to implement the new interface. +- Remove the `canCreateServiceWithName()` and `createServiceWithName()` methods + from your implementation. + +From our example above, we would update the class to read as follows: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Factory\AbstractFactoryInterface; // <-- note the change! + +class LenientAbstractFactory implements AbstractFactoryInterface +{ + public function canCreate(ContainerInterface $container, $requestedName) + { + return class_exists($requestedName); + } + + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + return new $requestedName(); + } +} +``` + +### DelegatorFactoryInterface + +The previous signature of the `DelegatorFactoryInterface` was: + +```php +interface DelegatorFactoryInterface +{ + /** + * A factory that creates delegates of a given service + * + * @param ServiceLocatorInterface $serviceLocator the service locator which requested the service + * @param string $name the normalized service name + * @param string $requestedName the requested service name + * @param callable $callback the callback that is responsible for creating the service + * + * @return mixed + */ + public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback); +} +``` + +The new signature is: + +```php +interface DelegatorFactoryInterface +{ + /** + * A factory that creates delegates of a given service + * + * @param ContainerInterface $container + * @param string $name + * @param callable $callback + * @param null|array $options + * @return object + */ + public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null); +} +``` + +Note that the `$name` and `$requestedName` arguments are now merged into a +single `$name` argument, and that the factory now allows passing additional +options to use (typically as passed via `build()`). + +To prepare your existing delegator factories for version 3, take the following +steps: + +- Implement the `__invoke()` method in your existing factory, copying the code + from your existing `createDelegatorWithName()` method into it. +- Modify the `createDelegatorWithName()` method to proxy to the new method. + +Consider the following delegator factory that works for version 2: + +```php +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ObserverAttachmentDelegator implements DelegatorFactoryInterface +{ + public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback) + { + $subject = $callback(); + $subject->attach($serviceLocator->get(Observer::class); + return $subject; + } +} +``` + +To prepare this for version 3, we'd implement the `__invoke()` signature from +version 3, and modify `createDelegatorWithName()` to proxy to it: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\DelegatorFactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class ObserverAttachmentDelegator implements DelegatorFactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null) + { + $subject = $callback(); + $subject->attach($container->get(Observer::class); + return $subject; + } + + public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback) + { + return $this($serviceLocator, $requestedName, $callback); + } +} +``` + +After you have upgraded to version 3, you can take the following steps to remove +the migration artifacts: + +- Update your class to implement the new interface. +- Remove the `createDelegatorWithName()` method from your implementation. + +From our example above, we would update the class to read as follows: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; // <-- note the change! + +class ObserverAttachmentDelegator implements DelegatorFactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null) + { + $subject = $callback(); + $subject->attach($container->get(Observer::class); + return $subject; + } +} +``` + +### FactoryInterface + +The previous signature of the `FactoryInterface` was: + +```php +interface FactoryInterface +{ + /** + * Create service + * + * @param ServiceLocatorInterface $serviceLocator + * @return mixed + */ + public function createService(ServiceLocatorInterface $serviceLocator); +} +``` + +The new signature is: + +```php +interface FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container + * @param string $requestedName + * @param null|array $options + * @return object + */ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null); +} +``` + +Note that the factory now accepts an additional *required* argument, +`$requestedName`; v2 already passed this argument, but it was not specified in +the interface itself. Additionally, a third *optional* argument, `$options`, +allows you to provide `$options` to the `ServiceManager::build()` method; +factories can then take these into account when creating an instance. + +Because factories now can expect to receive the service name, they may be +re-used for multiple services, largely replacing abstract factories in version +3. + +To prepare your existing factories for version 3, take the following steps: + +- Implement the `__invoke()` method in your existing factory, copying the code + from your existing `createService()` method into it. +- Modify the `createService()` method to proxy to the new method. + +Consider the following factory that works for version 2: + +```php +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class FooFactory implements FactoryInterface +{ + public function createService(ServiceLocatorInterface $services) + { + return new Foo($services->get(Bar::class)); + } +} +``` + +To prepare this for version 3, we'd implement the `__invoke()` signature from +version 3, and modify `createService()` to proxy to it: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class FooFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + return new Foo($container->get(Bar::class)); + } + + public function createService(ServiceLocatorInterface $services) + { + return $this($services, Foo::class); + } +} +``` + +Note that the call to `$this()` adds a new argument; since your factory isn't +using the `$requestedName`, this can be anything, but must be passed to prevent +a fatal exception due to a missing argument. In this case, we chose to pass the +name of the class the factory is creating. + +After you have upgraded to version 3, you can take the following steps to remove +the migration artifacts: + +- Update your class to implement the new interface. +- Remove the `createService()` method from your implementation. + +From our example above, we would update the class to read as follows: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Factory\FactoryInterface; // <-- note the change! + +class FooFactory implements FactoryInterface +{ + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + return new Foo($container->get(Bar::class)); + } +} +``` + +> #### Many factories already work with v3! +> +> Within the skeleton application, tutorial, and even in commonly shipped +> modules such as those in Laminas API Tools, we have typically suggested building your +> factories as invokable classes. If you were doing this already, your factories +> will already work with version 3! + +> #### Version 2 factories can accept the requested name already +> +> Since 2.2, factories have been passed two additional parameters, the +> "canonical" name (a mis-nomer, as it is actually the normalized name), and the +> "requested" name (the actual string passed to `get()`). As such, you can +> already write factories that accept the requested name, and have them +> change behavior based on that information! + +### New InvokableFactory Class + +`Laminas\ServiceManager\Factory\InvokableFactory` is a new `FactoryInterface` +implementation that provides the capabilities of the "invokable classes" present +in version 2. It essentially instantiates and returns the requested class name; +if `$options` is non-empty, it passes them directly to the constructor. + +This class was [added to the version 2 tree](https://github.com/zendframework/zend-servicemanager/pull/60) +to allow developers to start using it when preparing their code for version 3. +This is particularly of interest when creating plugin managers, as you'll +typically want the internal configuration to only include factories and aliases. + +## Initializers + +Initializers are still present in the Service Manager component, but exist +primarily for backwards compatibility; we recommend using delegator factories +for setter and interface injection instead of initializers, as those will be run +per-service, versus for all services. + +For migration purposes, the original interface was retained, and now inherits +from the new interface. This provides a migration path; you can add the method +defined in the new interface to your existing initializers targeting v2, and +safely upgrade. (Typically, you will then have the version 2 method proxy to +the one defined in version 3.) + +The following changes were made to initializers: + +- `Laminas\ServiceManager\InitializerInterface` was renamed to + `Laminas\ServiceManager\Initializer\InitializerInterface`. +- The interface itself has a new signature. + +The previous signature was: + +```php +public function initialize($instance, ServiceLocatorInterface $serviceLocator) +``` + +It is now: + +```php +public function __invoke(ContainerInterface $container, $instance) +``` + +The changes were made to ensure the signature is internally consistent with the +various factories. + +To prepare your existing initializers for version 3, take the following steps: + +- Implement the `__invoke()` method in your existing factory, copying the code + from your existing `initialize()` method into it. +- Modify the `initialize()` method to proxy to the new method. + +As an example, consider this initializer for version 2: + +```php +use Laminas\ServiceManager\InitializerInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class FooInitializer implements InitializerInterface +{ + public function initializer($instance, ServiceLocatorInterface $services) + { + if (! $instance implements FooAwareInterface) { + return $instance; + } + $instance->setFoo($services->get(FooInterface::class); + return $instance; + } +} +``` + +To prepare this for version 3, we'd implement the `__invoke()` signature from +version 3, and modify `initialize()` to proxy to it: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\InitializerInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class FooInitializer implements InitializerInterface +{ + public function __invoke(ContainerInterface $container, $instance) + { + if (! $instance implements FooAwareInterface) { + return $instance; + } + $container->setFoo($services->get(FooInterface::class); + return $instance; + } + + public function initializer($instance, ServiceLocatorInterface $services) + { + return $this($services, $instance); + } +} +``` + +After you have upgraded to version 3, you can take the following steps to remove +the migration artifacts: + +- Update your class to implement the new interface. +- Remove the `initialize()` method from your implementation. + +From our example above, we would update the class to read as follows: + +```php +use Interop\Container\ContainerInterface; +use Laminas\ServiceManager\Initializer\InitializerInterface; // <-- note the change! + +class FooInitializer implements InitializerInterface +{ + public function __invoke(ContainerInterface $container, $instance) + { + if (! $instance implements FooAwareInterface) { + return $instance; + } + $container->setFoo($services->get(FooInterface::class); + return $instance; + } +} +``` + +> ### Update your callables! +> +> Version 2 allows you to provide initializers as PHP callables. However, this +> means that the signature of those callables is incorrect for version 3! +> +> To make your code forwards compatible, you have two paths: +> +> The first is to simply provide an `InitializerInterface` implementation +> instead. This guarantees that the correct method is called based on the +> version of the `ServiceManager` in use. +> +> The second approach is to omit typehints on the arguments, and do typechecks +> internally. As an example, let's say you have the following: +> +> ```php +> $container->addInitializer(function ($instance, ContainerInterface $container) { +> if (! $instance implements FooAwareInterface) { +> return $instance; +> } +> $container->setFoo($services->get(FooInterface::class); +> return $instance; +> }); +> ``` +> +> To make this future-proof, remove the typehints, and check the types within +> the callable: +> +> ```php +> $container->addInitializer(function ($first, $second) { +> if ($first instanceof ContainerInterface) { +> $container = $first; +> $instance = $second; +> } else { +> $container = $second; +> $instance = $first; +> } +> if (! $instance implements FooAwareInterface) { +> return; +> } +> $container->setFoo($services->get(FooInterface::class); +> }); +> ``` +> +> This approach can also be done if you omitted typehints in the first place. +> Regardless, the important part to remember is that order of arguments is +> inverted between the two versions. + +## Plugin Managers + +In version 2, plugin managers were `ServiceManager` instances that implemented +both the `MutableCreationOptionsInterface` and `ServiceLocatorAwareInterface`, +and extended `AbstractPluginManager`. Plugin managers passed themselves to +factories, abstract factories, etc., requiring pulling the parent service +manager, if composed, in order to resolve application-level dependencies. + +In version 3, we define the following: + +- `Laminas\ServiceManager\PluginManagerInterface`, which provides the public API + differences from the `ServiceLocatorInterface`. +- `Laminas\ServiceManager\AbstractPluginManager`, which gives the basic + capabilities for plugin managers. The class now has a (semi) *required* + dependency on the application-level service manager instance, which is passed + to all factories, abstract factories, etc. (More on this below.) + +### PluginManagerInterface + +`Laminas\ServiceManager\PluginInterface` is a new interface for version 3, +extending `ServiceLocatorInterface` and adding one method: + +```php +/** + * Validate an instance + * + * @param object $instance + * @return void + * @throws InvalidServiceException If created instance does not respect the + * constraint on type imposed by the plugin manager + */ +public function validate($instance); +``` + +All plugin managers *must* implement this interface. For backwards-compatibility +purposes, `AbstractPluginManager` will check for the `validatePlugin()` method +(defined as abstract in v2), and, on discovery, trigger an `E_USER_DEPRECATED` +notice, followed by invocation of that method. + +### AbstractPluginManager + +As it did in version 2, `AbstractPluginManager` extends `ServiceManager`. **That +means that all changes made to the `ServiceManager` for v3 also apply to the +`AbstractPluginManager`.** + +In addition, review the following changes. + +#### Constructor + +- The constructor now accepts the following arguments, in the following order: + - The parent container instance; this is usually the application-level + `ServiceManager` instance. + - Optionally, an array of configuration for the plugin manager instance; this + should have the same format as for a `ServiceManager` instance. +- `validatePlugin()` was renamed to `validate()` (now defined in + `PluginManagerInterface`). The `AbstractPluginManager` provides + a basic implementation (detailed below). +- The signature of `get()` changes (more below). + +For backwards compatibility purposes, the constructor *also* allows the +following for the initial argument: + +- A `null` value. In this case, the plugin manager will use itself as the + creation context, *but also raise a deprecation notice indicating a + container should be passed instead.* You can pass the parent container + to the `setServiceLocator()` method to reset the creation context, but, + again, this raises a deprecation notice. +- A `ConfigInterface` instance. In this case, the plugin manager will call + the config instance's `toArray()` method to cast it to an array, and use the + return value as the configuration to pass to the parent constructor. As with + the `null` value, the plugin manager will be set as its own creation context. + +#### Validation + +The `validate()` method is defined as follows: + +```php +public function validate($instance) +{ + if (method_exists($this, 'validatePlugin')) { + trigger_error(sprintf( + '%s::validatePlugin() has been deprecated as of 3.0; please define validate() instead', + get_class($this) + ), E_USER_DEPRECATED); + $this->validatePlugin($instance); + return; + } + + if (empty($this->instanceOf) || $instance instanceof $this->instanceOf) { + return; + } + + throw new InvalidServiceException(sprintf( + 'Plugin manager "%s" expected an instance of type "%s", but "%s" was received', + __CLASS__, + $this->instanceOf, + is_object($instance) ? get_class($instance) : gettype($instance) + )); +} +``` + +The two takeaways from this are: + +- If you are upgrading from v2, your code should continue to work, *but will + emit a deprecation notice*. The way to remove the deprecation notice is to + rename the `validatePlugin()` method to `validate()`, or to remove it and + define the `$instanceOf` property (if all you're doing is checking the + plugin against a single typehint). +- Most plugin manager instances can simply define the `$instanceOf` property to + indicate what plugin interface is considered valid for the plugin manager, and + make no further changes to the abstract plugin manager: + +```php +protected $instanceOf = ValidatorInterface::class; +``` + +#### get() + +The `get()` signature changes from: + +```php +public function get($name, $options = [], $usePeeringServiceManagers = true) +``` + +to: + +```php +public function get($name, array $options = null) +``` + +Essentially: `$options` now *must* be an array if passed, and peering is no +longer supported. + +#### Deprecated methods + +Finally, the following methods from v2's `ServiceLocatorAwareInterface` are +retained (without implementing the interface), but marked as deprecated: + +- `setServiceLocator()`. This method exists as many tests and plugin manager + factories were using it to inject the parent locator (now called the creation + context). This method may still be used, and will now set the creation context + for the plugin manager, but also emit a deprecation warning. +- `getServiceLocator()` is implemented in `ServiceManager` (from which + `AbstractPluginManager` inherits), but marked as deprecated. + +Regarding this latter point, `getServiceLocator()` exists to provide backwards +compatibility *for existing plugin factories*. These factories typically pull +dependencies from the parent/application container in order to initialize the +plugin. In v2, this would look like: + +```php +function ($plugins) +{ + $services = $plugins->getServiceLocator(); + + // pull dependencies from $services: + $foo = $services->get('Foo'); + $bar = $services->get('Bar'); + + return new Plugin($foo, $bar); +} +``` + +In v3, the initial argument to the factory is not the plugin manager instance, +but the *creation context*, which is analogous to the parent locator in v2. In +order to preserve existing behavior, we added the `getServiceLocator()` method +to the `ServiceManager`. As such, the above will continue to work in v3. + +However, this method is marked as deprecated, and will emit an +`E_USER_DEPRECATED` notice. To remove the notice, you will need to upgrade your +code. The above example thus becomes: + +```php +function ($services) +{ + // pull dependencies from $services: + $foo = $services->get('Foo'); + $bar = $services->get('Bar'); + + return new Plugin($foo, $bar); +} +``` + +If you *were* using the passed plugin manager and pulling other plugins, you +will need to update your code to retrieve the plugin manager from the passed +container. As an example, given this: + +```php +function ($plugins) +{ + $anotherPlugin = $plugins->get('AnotherPlugin'); + return new Plugin($anotherPlugin); +} +``` + +You will need to rewrite it to: + +```php +function ($services) +{ + $plugins = $services->get('PluginManager'); + $anotherPlugin = $plugins->get('AnotherPlugin'); + return new Plugin($anotherPlugin); +} +``` + +### Plugin Service Creation + +The `get()` method has new behavior: + +- When non-empty `$options` are passed, it *always* delegates to `build()`, and + thus will *always* return a *new instance*. If you are using `$options`, the + assumption is that you are using the plugin manager as a factory, and thus the + instance should not be cached. +- Without `$options`, `get()` will cache by default (the default behavior of + `ServiceManager`). To *never* cache instances, either set the + `$sharedByDefault` class property to `false`, or pass a boolean `false` value + via the `shared_by_default` configuration key. + +### Migration example + +Let's consider the following plugin manager geared towards version 2: + +```php +use RuntimeException; +use Laminas\ServiceManager\AbstractPluginManager; + +class ObserverPluginManager extends AbstractPluginManager +{ + protected $invokables = [ + 'mail' => MailObserver::class, + 'log' => LogObserver::class, + ]; + + protected $shareByDefault = false; + + public function validatePlugin($instance) + { + if (! $instance instanceof ObserverInterface) { + throw new RuntimeException(sprintf( + 'Invalid plugin "%s" created; not an instance of %s', + get_class($instance), + ObserverInterface::class + )); + } + } +} +``` + +To prepare this for version 3, we need to do the following: + +- We need to change the `$invokables` configuration to a combination of + `factories` and `aliases`. +- We need to implement a `validate()` method. +- We need to update the `validatePlugin()` method to proxy to `validate()`. +- We need to add a `$sharedByDefault` property (if `$shareByDefault` is present). + +Doing so, we get the following result: + +```php +namespace MyNamespace; + +use RuntimeException; +use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\Exception\InvalidServiceException; +use Laminas\ServiceManager\Factory\InvokableFactory; + +class ObserverPluginManager extends AbstractPluginManager +{ + protected $instanceOf = ObserverInterface::class; + + protected $aliases = [ + 'mail' => MailObserver::class, + 'Mail' => MailObserver::class, + 'log' => LogObserver::class, + 'Log' => LogObserver::class, + ]; + + protected $factories = [ + MailObserver::class => InvokableFactory::class, + LogObserver::class => InvokableFactory::class, + // Legacy (v2) due to alias resolution + 'mynamespacemailobserver' => InvokableFactory::class, + 'mynamespacelogobserver' => InvokableFactory::class, + ]; + + protected $shareByDefault = false; + + protected $sharedByDefault = false; + + public function validate($instance) + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + 'Invalid plugin "%s" created; not an instance of %s', + get_class($instance), + $this->instanceOf + )); + } + } + + public function validatePlugin($instance) + { + try { + $this->validate($instance); + } catch (InvalidServiceException $e) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } +} +``` + +Things to note about the above: + +- It introduces a new property, `$instanceOf`. We'll use this later, when we're + ready to clean up post-migration. +- It introduces four aliases. This is to allow fetching the various plugins as + any of `mail`, `Mail`, `log`, or `Log` — all of which are valid in + version 2, but, because version 3 does not normalize names, need to be + explicitly aliased. +- The aliases point to the fully qualified class name (FQCN) for the service + being generated, and these are mapped to `InvokableFactory` instances. This + means you can also fetch your plugins by their FQCN. +- There are also factory entries for the canonicalized FQCN of each factory, + which will be used in v2. (Canonicalization in v2 strips non-alphanumeric + characters, and casts to lowercase.) +- `validatePlugin()` continues to throw the old exception + +The above will now work in both version 2 and version 3. + +### Migration testing + +To test your changes, create a new `MigrationTest` case that uses +`Laminas\ServiceManager\Test\CommonPluginManagerTrait`. Override +`getPluginManager()` to return an instance of your plugin manager, and override +`getV2InvalidPluginException()` to return the classname of the exception your +`validatePlugin()` method throws: + +```php +use MyNamespace\ObserverInterface; +use MyNamespace\ObserverPluginManager; +use MyNamespace\Exception\RuntimeException; +use PHPUnit_Framework_TestCase as TestCase; +use Laminas\ServiceManager\ServiceManager; +use Laminas\ServiceManager\Test\CommonPluginManagerTrait; + +class MigrationTest extends TestCase +{ + use CommonPluginManagerTrait; + + protected function getPluginManager() + { + return new ObserverPluginManager(new ServiceManager()); + } + + protected function getV2InvalidPluginException() + { + return RuntimeException::class; + } + + protected function getInstanceOf() + { + return ObserverInterface::class; + } +} +``` + +This will check that: + +- You have set the `$instanceOf` property. +- `$shareByDefault` and `$sharedByDefault` match, if present. +- That requesting an invalid plugin throws the right exception. +- That all your aliases resolve. + + +### Post migration + +After you migrate to version 3, you can clean up your plugin manager: + +- Remove the `validatePlugin()` method. +- If your `validate()` routine is only checking that the instance is of a single + type, and has no other logic, you can remove that implementation as well, as + the `AbstractPluginManager` already takes care of that when `$instanceOf` is + defined! +- Remove the canonicalized FQCN entry for each factory + +Performing these steps on the above, we get: + +```php +use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\Factory\InvokableFactory; + +class ObserverPluginManager extends AbstractPluginManager +{ + protected $instanceOf = ObserverInterface::class; + + protected $aliases = [ + 'mail' => MailObserver::class, + 'Mail' => MailObserver::class, + 'log' => LogObserver::class, + 'Log' => LogObserver::class, + ]; + + protected $factories = [ + MailObserver::class => InvokableFactory::class, + LogObserver::class => InvokableFactory::class, + ]; +} +``` + +## DI Namespace + +**The `Laminas\ServiceManager\Di` namespace has been removed.** + +The `Laminas\Di` component is not actively maintained, and has been largely +deprecated during the Laminas lifecycle in favor of the Service Manager. Its usage +as an abstract factory is problematic and error prone when used in conjunction +with the Service Manager; as such, we've removed it for the initial v3 release. + +We may re-introduce it via a separate component in the future. + +## Miscellaneous Interfaces, Traits, and Classes + +The following interfaces, traits, and classes were *removed*: + +- `Laminas\ServiceManager\MutableCreationOptionsInterface`; this was previously + used by the `AbstractPluginManager`, and is no longer required as we ship a + separate `PluginManagerInterface`, and because the functionality is + encompassed by the `build()` method. +- `Laminas\ServiceManager\MutableCreationOptionsTrait` +- `Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory`; its capabilities were + moved directly into the `ServiceManager`. +- `Laminas\ServiceManager\ServiceLocatorAwareInterface` +- `Laminas\ServiceManager\ServiceLocatorAwareTrait` +- `Laminas\ServiceManager\ServiceManagerAwareInterface` + +The `ServiceLocatorAware` and `ServiceManagerAware` interfaces and traits were +too often abused under v2, and represent the antithesis of the purpose of the +Service Manager component; dependencies should be directly injected, and the +container should never be composed by objects. + +The following classes and interfaces have changes: + +- `Laminas\ServiceManager\Proxy\LazyServiceFactory` is now marked `final`, and + implements `Laminas\ServiceManager\Proxy\DelegatorFactoryInterface`. Its + dependencies and capabilities remain the same. +- `Laminas\ServiceManager\ConfigInterface` now is expected to *return* the modified + `ServiceManager` instance. +- `Laminas\ServiceManager\Config` was updated to follow the changes to + `ConfigInterface` and `ServiceManager`, and now returns the updated + `ServiceManager` instance from `configureServiceManager()`. diff --git a/docs/book/plugin-managers.md b/docs/book/v4/plugin-managers.md similarity index 100% rename from docs/book/plugin-managers.md rename to docs/book/v4/plugin-managers.md diff --git a/docs/book/v4/psr-11.md b/docs/book/v4/psr-11.md new file mode 100644 index 00000000..c0bfe98f --- /dev/null +++ b/docs/book/v4/psr-11.md @@ -0,0 +1,55 @@ +# PSR-11 Support + +[container-interop/container-interop 1.2.0](https://github.com/container-interop/container-interop/releases/tag/1.2.0) +modifies its codebase to extend interfaces from [psr/container](https://github.com/php-fig/container) +(the official interfaces for [PSR-11](http://www.php-fig.org/psr/psr-11/)). If +you are on a pre-3.3.0 version of laminas-servicemanager, update your project, and +receive container-interop 1.2, then laminas-servicemanager can already act as a +PSR-11 provider! + +laminas-servicemanager 3.3.0 requires at least version 1.2 of container-interop, +and _also_ requires psr/container 1.0 to explicitly signal that it is a PSR-11 +provider, and to allow removal of the container-interop dependency later. + +Version 4.0 will require only psr/container, and will update the various factory +interfaces and exception implementations to typehint against the PSR-11 +interfaces, which will require changes to any implementations you have. In the +meantime, you can [duck-type](https://en.wikipedia.org/wiki/Duck_typing) the +following factory types: + +- `Laminas\ServiceManager\Factory\FactoryInterface`: use a callable with the + following signature: + ```php + function ( + \Psr\Container\ContainerInterface $container, + string $requestedName, + array $options = null + ) + ``` + +- `Laminas\ServiceManager\Factory\DelegatorFactoryInterface`: use a callable with + the following signature: + ```php + function ( + \Psr\Container\ContainerInterface $container, + string $name, + callable $callback, + array $options = null + ) + ``` + +- `Laminas\ServiceManager\Initializer\InitializerInterface`: use a callable with + the following signature: + ```php + function ( + \Psr\Container\ContainerInterface $container, + $instance + ) + ``` + +Abstract factories _can not_ be duck typed, due to the additional `canCreate()` +method. + +You can also leave your factories as-is for now, and update them once +laminas-servicemanager v4.0 is released, at which time we will be providing tooling +to help migrate your factories to PSR-11. diff --git a/docs/book/v4/quick-start.md b/docs/book/v4/quick-start.md new file mode 100644 index 00000000..c9246dad --- /dev/null +++ b/docs/book/v4/quick-start.md @@ -0,0 +1,67 @@ +# Quick Start + +The Service Manager is a modern, fast, and easy-to-use implementation of the +[Service Locator design pattern](https://en.wikipedia.org/wiki/Service_locator_pattern). +The implementation implements the +[Container Interop](https://github.com/container-interop/container-interop) +interfaces, providing interoperability with other implementations. + +The following is a "quick start" tutorial intended to get you up and running +with the most common features of the Service manager. + +## 1. Install Laminas Service Manager + +If you haven't already, [install Composer](https://getcomposer.org). Once you +have, you can install the service manager: + +```bash +$ composer require laminas/laminas-servicemanager +``` + +## 2. Configuring a service manager + +You can now create and configure a service manager. The service manager +constructor accepts a simple array: + +```php +use Laminas\ServiceManager\ServiceManager; +use Laminas\ServiceManager\Factory\InvokableFactory; +use stdClass; + +$serviceManager = new ServiceManager([ + 'factories' => [ + stdClass::class => InvokableFactory::class, + ], +]); +``` + +The service manager accepts a variety of keys; refer to the +[Configuring service manager](configuring-the-service-manager.md) section for +full details. + +## 3. Retrieving objects + +Finally, you can retrieve instances using the `get()` method: + +```php +$object = $serviceManager->get(stdClass::class); +``` + +By default, all objects created through the service manager are shared. This +means that calling the `get()` method twice will return the exact same object: + +```php +$object1 = $serviceManager->get(stdClass::class); +$object2 = $serviceManager->get(stdClass::class); + +var_dump($object1 === $object2); // prints "true" +``` + +You can use the `build()` method to retrieve discrete instances for a service: + +```php +$object1 = $serviceManager->build(stdClass::class); +$object2 = $serviceManager->build(stdClass::class); + +var_dump($object1 === $object2); // prints "false" +``` diff --git a/docs/book/reflection-abstract-factory.md b/docs/book/v4/reflection-abstract-factory.md similarity index 100% rename from docs/book/reflection-abstract-factory.md rename to docs/book/v4/reflection-abstract-factory.md From 95ba1f0d8e96e614a999153cc592a4c4d3d0b3ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Thu, 6 Apr 2023 00:23:29 +0200 Subject: [PATCH 08/32] docs: reference correct file so that mkdocs will generate proper link to that page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/ahead-of-time-factories.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v4/ahead-of-time-factories.md b/docs/book/v4/ahead-of-time-factories.md index cef78d94..ae89e947 100644 --- a/docs/book/v4/ahead-of-time-factories.md +++ b/docs/book/v4/ahead-of-time-factories.md @@ -2,7 +2,7 @@ - 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. +In addition to the already existing [Reflection Factory](reflection-abstract-factory.md), one can create factories for those services using `ReflectionBasedAbstractFactory` before deploying the project to production. 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. From 9216f52d4f73baec4d4f2cd966de3cd1214947ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:35:16 +0200 Subject: [PATCH 09/32] docs: remove superfluous backtick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/ahead-of-time-factories.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v4/ahead-of-time-factories.md b/docs/book/v4/ahead-of-time-factories.md index ae89e947..ccf34d98 100644 --- a/docs/book/v4/ahead-of-time-factories.md +++ b/docs/book/v4/ahead-of-time-factories.md @@ -23,7 +23,7 @@ When the CLI command has finished, there are all factories generated within the 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 `` 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. +Ensure that both `` 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 From 86b33a8d34c7665dd86d4b0787a3f6b3244d6d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:37:31 +0200 Subject: [PATCH 10/32] docs: remove superfluous whitespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 794a3402..862c83f0 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@ [![Psalm coverage](https://shepherd.dev/github/laminas/laminas-servicemanager/coverage.svg?)](https://shepherd.dev/github/laminas/laminas-servicemanager) > ## 🇷🇺 Русским гражданам -> +> > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм. -> +> > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую. -> +> > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!" -> +> > ## 🇺🇸 To Citizens of Russia -> +> > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism. -> +> > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences. -> +> > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!" The Service Locator design pattern is implemented by the `Laminas\ServiceManager` From d35caf5618e15fe9268b511684099b9f7bac135d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:40:38 +0200 Subject: [PATCH 11/32] docs: normalize some parts of the migration guide to match markdown rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v3/migration.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/book/v3/migration.md b/docs/book/v3/migration.md index 37950abf..482b2374 100644 --- a/docs/book/v3/migration.md +++ b/docs/book/v3/migration.md @@ -1,6 +1,6 @@ # Migration Guide -The Service Manager was first introduced for Laminas.0.0. Its API +The Service Manager was first introduced for Laminas 2.0.0. Its API remained the same throughout that version. Version 3 is the first new major release of the Service Manager, and contains a @@ -116,7 +116,7 @@ version 2 in version 3. However, we recommend starting to update your configuration to remove `invokables` entries in favor of factories (and aliases, if needed). -> #### Invokables and plugin managers +> ### Invokables and plugin managers > > If you are creating a plugin manager and in-lining invokables into the class > definition, you will need to make some changes. @@ -718,7 +718,7 @@ class FooFactory implements FactoryInterface } ``` -> #### Many factories already work with v3! +> #### Many factories already work with v3 > > Within the skeleton application, tutorial, and even in commonly shipped > modules such as those in Laminas API Tools, we have typically suggested building your @@ -1276,7 +1276,6 @@ This will check that: - That requesting an invalid plugin throws the right exception. - That all your aliases resolve. - ### Post migration After you migrate to version 3, you can clean up your plugin manager: From 6c5361b4ecaf8aa49df064ab93443b5a21373b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:41:11 +0200 Subject: [PATCH 12/32] docs: purge migration guide for v4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/migration.md | 1354 +------------------------------------ 1 file changed, 1 insertion(+), 1353 deletions(-) diff --git a/docs/book/v4/migration.md b/docs/book/v4/migration.md index 37950abf..c3b0fb29 100644 --- a/docs/book/v4/migration.md +++ b/docs/book/v4/migration.md @@ -1,1355 +1,3 @@ # Migration Guide -The Service Manager was first introduced for Laminas.0.0. Its API -remained the same throughout that version. - -Version 3 is the first new major release of the Service Manager, and contains a -number of backwards compatibility breaks. These were introduced to provide -better performance and stability. - -## Case Sensitivity and Normalization - -v2 normalized service names as follows: - -- It stripped non alphanumeric characters. -- It lowercased the resulting string. - -This was done to help prevent typographical errors from creating configuration -errors. However, it also presented a large performance hit, and led to some -unexpected behaviors. - -**In v3, service names are case sensitive, and are not normalized in any way.** - -As such, you *must* refer to services using the same case in which they were -registered. - -## Configuration - -A number of changes have been made to configuration of service and plugin -managers: - -- Minor changes in configuration arrays may impact your usage. -- `ConfigInterface` implementations and consumers will need updating. - -### Configuration arrays - -Configuration for v2 consisted of the following: - -```php -[ - 'services' => [ - // service name => instance pairs - ], - 'aliases' => [ - // alias => service name pairs - ], - 'invokables' => [ - // service name => class name pairs - ], - 'factories' => [ - // service name => factory pairs - ], - 'abstract_factories' => [ - // abstract factories - ], - 'initializers' => [ - // initializers - ], - 'delegators' => [ - // service name => [ delegator factories ] - ], - 'shared' => [ - // service name => boolean - ], - 'share_by_default' => boolean, -] -``` - -In v3, the configuration remains the same, with the following additions: - -```php -[ - 'lazy_services' => [ - // The class_map is required if using lazy services: - 'class_map' => [ - // service name => class name pairs - ], - // The following are optional: - 'proxies_namespace' => 'Alternate namespace to use for generated proxy classes', - 'proxies_target_dir' => 'path in which to write generated proxy classes', - 'write_proxy_files' => true, // boolean; false by default - ], -] -``` - -The main change is the addition of integrated lazy service configuration is now -integrated. - -### ConfigInterface - -The principal change to the `ConfigInterface` is the addition of the -`toArray()` method. This method is intended to return a configuration array in -the format listed above, for passing to either the constructor or the -`configure()` method of the `ServiceManager`.. - -### Config class - -`Laminas\ServiceManager\Config` has been updated to follow the changes to the -`ConfigInterface` and `ServiceManager`. This essentially means that it removes -the various getter methods, and adds the `toArray()` method. - -## Invokables - -*Invokables no longer exist,* at least, not identically to how they existed in -Laminas. - -Internally, `ServiceManager` now does the following for `invokables` entries: - -- If the name and value match, it creates a `factories` entry mapping the - service name to `Laminas\ServiceManager\Factory\InvokableFactory`. -- If the name and value *do not* match, it creates an `aliases` entry mapping the - service name to the class name, *and* a `factories` entry mapping the class - name to `Laminas\ServiceManager\Factory\InvokableFactory`. - -This means that you can use your existing `invokables` configuration from -version 2 in version 3. However, we recommend starting to update your -configuration to remove `invokables` entries in favor of factories (and aliases, -if needed). - -> #### Invokables and plugin managers -> -> If you are creating a plugin manager and in-lining invokables into the class -> definition, you will need to make some changes. -> -> `$invokableClasses` will need to become `$factories` entries, and you will -> potentially need to add `$aliases` entries. -> -> As an example, consider the following, from laminas-math v2.x: -> -> ```php -> class AdapterPluginManager extends AbstractPluginManager -> { -> protected $invokableClasses = [ -> 'bcmath' => Adapter\Bcmath::class, -> 'gmp' => Adapter\Gmp::class, -> ]; -> } -> ``` -> -> Because we no longer define an `$invokableClasses` property, for v3.x, this -> now becomes: -> -> ```php -> use Laminas\ServiceManager\Factory\InvokableFactory; -> -> class AdapterPluginManager extends AbstractPluginManager -> { -> protected $aliases = [ -> 'bcmath' => Adapter\Bcmath::class, -> 'gmp' => Adapter\Gmp::class, -> ]; -> -> protected $factories = [ -> Adapter\BcMath::class => InvokableFactory::class, -> Adapter\Gmp::class => InvokableFactory::class, -> ]; -> } -> ``` - -## Lazy Services - -In v2, if you wanted to create a lazy service, you needed to take the following -steps: - -- Ensure you have a `config` service, with a `lazy_services` key that contained - the configuration necessary for the `LazyServiceFactory`. -- Assign the `LazyServiceFactoryFactory` as a factory for the - `LazyServiceFactory` -- Assign the `LazyServiceFactory` as a delegator factory for your service. - -As an example: - -```php -use Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory; - -$config = [ - 'lazy_services' => [ - 'class_map' => [ - 'MyClass' => 'MyClass', - ], - 'proxies_namespace' => 'TestAssetProxy', - 'proxies_target_dir' => 'data/proxies/', - 'write_proxy_files' => true, - ], -]; - -return [ - 'services' => [ - 'config' => $config, - ], - 'invokables' => [ - 'MyClass' => 'MyClass', - ], - 'factories' => [ - 'LazyServiceFactory' => LazyServiceFactoryFactory::class, - ], - 'delegators' => [ - 'MyClass' => [ - 'LazyServiceFactory', - ], - ], -]; -``` - -This was done in part because lazy services were introduced later in the v2 -cycle, and not fully integrated in order to retain the API. - -In order to reduce the number of dependencies and steps necessary to configure -lazy services, the following changes were made for v3: - -- Lazy service configuration can now be passed directly to the service manager; - it is no longer dependent on a `config` service. -- The ServiceManager itself is now responsible for creating the - `LazyServiceFactory` delegator factory, based on the configuration present. - -The above example becomes the following in v3: - -```php -use Laminas\ServiceManager\Factory\InvokableFactory; -use Laminas\ServiceManager\Proxy\LazyServiceFactory; - -return [ - 'factories' => [ - 'MyClass' => InvokableFactory::class, - ], - 'delegators' => [ - 'MyClass' => [ - LazyServiceFactory::class, - ], - ], - 'lazy_services' => [ - 'class_map' => [ - 'MyClass' => 'MyClass', - ], - 'proxies_namespace' => 'TestAssetProxy', - 'proxies_target_dir' => 'data/proxies/', - 'write_proxy_files' => true, - ], -]; -``` - -Additionally, assuming you have configured lazy services initially with the -proxy namespace, target directory, etc., you can map lazy services using the new -method `mapLazyService($name, $class)`: - -```php -$container->mapLazyService('MyClass', 'MyClass'); -// or, more simply: -$container->mapLazyService('MyClass'); -``` - -## ServiceLocatorInterface Changes - -The `ServiceLocatorInterface` now extends the -[container-interop](https://github.com/container-interop/container-interop) -interface `ContainerInterface`, which defines the same `get()` and `has()` -methods as were previously defined. - -Additionally, it adds a new method: - -```php -public function build($name, array $options = null) -``` - -This method is defined to *always* return a *new* instance of the requested -service, and to allow using the provided `$options` when creating the instance. - -## ServiceManager API Changes - -`Laminas\ServiceManager\ServiceManager` remains the primary interface with which -developers will interact. It has the following changes in v3: - -- It adds a new method, `configure()`, which allows configuring all instance - generation capabilities (aliases, factories, abstract factories, etc.) at - once. -- Peering capabilities were removed. -- Exceptions are *always* thrown when service instance creation fails or - produces an error; you can no longer disable this. -- Configuration no longer requires a `Laminas\ServiceManager\Config` instance. - `Config` can be used, but is not needed. -- It adds a new method, `build()`, for creating discrete service instances. - -### Methods Removed - -*The following methods are removed* in v3: - -- `setShareByDefault()`/`shareByDefault()`; this can be passed during - instantiation or via `configure()`. -- `setThrowExceptionInCreate()`/`getThrowExceptionInCreate()`; exceptions are - *always* thrown when errors are encountered during service instance creation. -- `setRetrieveFromPeeringManagerFirst()`/`retrieveFromPeeringManagerFirst()`; - peering is no longer supported. - -### Constructor - -The constructor now accepts an array of service configuration, not a -`Laminas\ServiceManager\Config` instance. - -### Use `build()` for discrete instances - -The new method `build()` acts as a factory method for configured services, and -will *always* return a new instance, never a shared one. - -Additionally, it provides factory capabilities; you may pass an additional, -optional argument, `$options`, which should be an array of additional options a -factory may use to create a new instance. This is primarily of interest when -creating plugin managers (more on plugin managers below), which may pass that -information on in order to create discrete plugin instances with specific state. - -As examples: - -```php -use Laminas\Validator\Between; - -$between = $container->build(Between::class, [ - 'min' => 5, - 'max' => 10, - 'inclusive' => true, -]); - -$alsoBetween = $container->build(Between::class, [ - 'min' => 0, - 'max' => 100, - 'inclusive' => false, -]); -``` - -The above two validators would be different instances, with their own -configuration. - -## Factories - -Internally, the `ServiceManager` now only uses the new factory interfaces -defined in the `Laminas\ServiceManager\Factory` namespace. These *replace* the -interfaces defined in version 2, and define completely new signatures. - -For migration purposes, all original interfaces were retained, and now inherit -from the new interfaces. This provides a migration path; you can add the methods -defined in the new interfaces to your existing factories targeting v2, and -safely upgrade. (Typically, you will then have the version 2 methods proxy to -those defined in version 3.) - -### Interfaces and relations to version 2 - -| Version 2 Interface | Version 3 Interface | -| :-------------------------------------------------------: | :-------------------------------------------------------: | -| `Laminas\ServiceManager\AbstractFactoryInterface` | `Laminas\ServiceManager\Factory\AbstractFactoryInterface` | -| `Laminas\ServiceManager\DelegatorFactoryInterface` | `Laminas\ServiceManager\Factory\DelegatorFactoryInterface` | -| `Laminas\ServiceManager\FactoryInterface` | `Laminas\ServiceManager\Factory\FactoryInterface` | - -The version 2 interfaces now extend those in version 3, but are marked -**deprecated**. You can continue to use them, but will be required to update -your code to use the new interfaces in the future. - -### AbstractFactoryInterface - -The previous signature of the `AbstractFactoryInterface` was: - -```php -interface AbstractFactoryInterface -{ - /** - * Determine if we can create a service with name - * - * @param ServiceLocatorInterface $serviceLocator - * @param $name - * @param $requestedName - * @return bool - */ - public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); - - /** - * Create service with name - * - * @param ServiceLocatorInterface $serviceLocator - * @param $name - * @param $requestedName - * @return mixed - */ - public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName); -} -``` - -The new signature is: - -```php -interface AbstractFactoryInterface extends FactoryInterface -{ - /** - * Does the factory have a way to create an instance for the service? - * - * @param ContainerInterface $container - * @param string $requestedName - * @return bool - */ - public function canCreate(ContainerInterface $container, $requestedName); -} -``` - -Note that it now *extends* the `FactoryInterface` (detailed below), and thus the -factory logic has the same signature. - -In v2, the abstract factory defined the method `canCreateServiceWithName()`; in -v3, this is renamed to `canCreate()`, and the method also now receives only two -arguments, the container and the requested service name. - -To prepare your version 2 implementation to work upon upgrade to version 3: - -- Add the methods `canCreate()` and `__invoke()` as defined in version 3. -- Modify your existing `canCreateServiceWithName()` method to proxy to - `canCreate()` -- Modify your existing `createServiceWithName()` method to proxy to - `__invoke()` - -As an example, given the following implementation from version 2: - -```php -use Laminas\ServiceManager\AbstractFactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class LenientAbstractFactory implements AbstractFactoryInterface -{ - public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) - { - return class_exists($requestedName); - } - - public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) - { - return new $requestedName(); - } -} -``` - -To update this for version 3 compatibility, you will add the methods -`canCreate()` and `__invoke()`, move the code from the existing methods into -them, and update the existing methods to proxy to the new methods: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\AbstractFactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class LenientAbstractFactory implements AbstractFactoryInterface -{ - public function canCreate(ContainerInterface $container, $requestedName) - { - return class_exists($requestedName); - } - - public function canCreateServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) - { - return $this->canCreate($services, $requestedName); - } - - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) - { - return new $requestedName(); - } - - public function createServiceWithName(ServiceLocatorInterface $services, $name, $requestedName) - { - return $this($services, $requestedName); - } -} -``` - -After you have upgraded to version 3, you can take the following steps to remove -the migration artifacts: - -- Update your class to implement the new interface. -- Remove the `canCreateServiceWithName()` and `createServiceWithName()` methods - from your implementation. - -From our example above, we would update the class to read as follows: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\Factory\AbstractFactoryInterface; // <-- note the change! - -class LenientAbstractFactory implements AbstractFactoryInterface -{ - public function canCreate(ContainerInterface $container, $requestedName) - { - return class_exists($requestedName); - } - - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) - { - return new $requestedName(); - } -} -``` - -### DelegatorFactoryInterface - -The previous signature of the `DelegatorFactoryInterface` was: - -```php -interface DelegatorFactoryInterface -{ - /** - * A factory that creates delegates of a given service - * - * @param ServiceLocatorInterface $serviceLocator the service locator which requested the service - * @param string $name the normalized service name - * @param string $requestedName the requested service name - * @param callable $callback the callback that is responsible for creating the service - * - * @return mixed - */ - public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback); -} -``` - -The new signature is: - -```php -interface DelegatorFactoryInterface -{ - /** - * A factory that creates delegates of a given service - * - * @param ContainerInterface $container - * @param string $name - * @param callable $callback - * @param null|array $options - * @return object - */ - public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null); -} -``` - -Note that the `$name` and `$requestedName` arguments are now merged into a -single `$name` argument, and that the factory now allows passing additional -options to use (typically as passed via `build()`). - -To prepare your existing delegator factories for version 3, take the following -steps: - -- Implement the `__invoke()` method in your existing factory, copying the code - from your existing `createDelegatorWithName()` method into it. -- Modify the `createDelegatorWithName()` method to proxy to the new method. - -Consider the following delegator factory that works for version 2: - -```php -use Laminas\ServiceManager\DelegatorFactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class ObserverAttachmentDelegator implements DelegatorFactoryInterface -{ - public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback) - { - $subject = $callback(); - $subject->attach($serviceLocator->get(Observer::class); - return $subject; - } -} -``` - -To prepare this for version 3, we'd implement the `__invoke()` signature from -version 3, and modify `createDelegatorWithName()` to proxy to it: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\DelegatorFactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class ObserverAttachmentDelegator implements DelegatorFactoryInterface -{ - public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null) - { - $subject = $callback(); - $subject->attach($container->get(Observer::class); - return $subject; - } - - public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback) - { - return $this($serviceLocator, $requestedName, $callback); - } -} -``` - -After you have upgraded to version 3, you can take the following steps to remove -the migration artifacts: - -- Update your class to implement the new interface. -- Remove the `createDelegatorWithName()` method from your implementation. - -From our example above, we would update the class to read as follows: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; // <-- note the change! - -class ObserverAttachmentDelegator implements DelegatorFactoryInterface -{ - public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null) - { - $subject = $callback(); - $subject->attach($container->get(Observer::class); - return $subject; - } -} -``` - -### FactoryInterface - -The previous signature of the `FactoryInterface` was: - -```php -interface FactoryInterface -{ - /** - * Create service - * - * @param ServiceLocatorInterface $serviceLocator - * @return mixed - */ - public function createService(ServiceLocatorInterface $serviceLocator); -} -``` - -The new signature is: - -```php -interface FactoryInterface -{ - /** - * Create an object - * - * @param ContainerInterface $container - * @param string $requestedName - * @param null|array $options - * @return object - */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null); -} -``` - -Note that the factory now accepts an additional *required* argument, -`$requestedName`; v2 already passed this argument, but it was not specified in -the interface itself. Additionally, a third *optional* argument, `$options`, -allows you to provide `$options` to the `ServiceManager::build()` method; -factories can then take these into account when creating an instance. - -Because factories now can expect to receive the service name, they may be -re-used for multiple services, largely replacing abstract factories in version -3. - -To prepare your existing factories for version 3, take the following steps: - -- Implement the `__invoke()` method in your existing factory, copying the code - from your existing `createService()` method into it. -- Modify the `createService()` method to proxy to the new method. - -Consider the following factory that works for version 2: - -```php -use Laminas\ServiceManager\FactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class FooFactory implements FactoryInterface -{ - public function createService(ServiceLocatorInterface $services) - { - return new Foo($services->get(Bar::class)); - } -} -``` - -To prepare this for version 3, we'd implement the `__invoke()` signature from -version 3, and modify `createService()` to proxy to it: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\FactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class FooFactory implements FactoryInterface -{ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) - { - return new Foo($container->get(Bar::class)); - } - - public function createService(ServiceLocatorInterface $services) - { - return $this($services, Foo::class); - } -} -``` - -Note that the call to `$this()` adds a new argument; since your factory isn't -using the `$requestedName`, this can be anything, but must be passed to prevent -a fatal exception due to a missing argument. In this case, we chose to pass the -name of the class the factory is creating. - -After you have upgraded to version 3, you can take the following steps to remove -the migration artifacts: - -- Update your class to implement the new interface. -- Remove the `createService()` method from your implementation. - -From our example above, we would update the class to read as follows: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\Factory\FactoryInterface; // <-- note the change! - -class FooFactory implements FactoryInterface -{ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) - { - return new Foo($container->get(Bar::class)); - } -} -``` - -> #### Many factories already work with v3! -> -> Within the skeleton application, tutorial, and even in commonly shipped -> modules such as those in Laminas API Tools, we have typically suggested building your -> factories as invokable classes. If you were doing this already, your factories -> will already work with version 3! - -> #### Version 2 factories can accept the requested name already -> -> Since 2.2, factories have been passed two additional parameters, the -> "canonical" name (a mis-nomer, as it is actually the normalized name), and the -> "requested" name (the actual string passed to `get()`). As such, you can -> already write factories that accept the requested name, and have them -> change behavior based on that information! - -### New InvokableFactory Class - -`Laminas\ServiceManager\Factory\InvokableFactory` is a new `FactoryInterface` -implementation that provides the capabilities of the "invokable classes" present -in version 2. It essentially instantiates and returns the requested class name; -if `$options` is non-empty, it passes them directly to the constructor. - -This class was [added to the version 2 tree](https://github.com/zendframework/zend-servicemanager/pull/60) -to allow developers to start using it when preparing their code for version 3. -This is particularly of interest when creating plugin managers, as you'll -typically want the internal configuration to only include factories and aliases. - -## Initializers - -Initializers are still present in the Service Manager component, but exist -primarily for backwards compatibility; we recommend using delegator factories -for setter and interface injection instead of initializers, as those will be run -per-service, versus for all services. - -For migration purposes, the original interface was retained, and now inherits -from the new interface. This provides a migration path; you can add the method -defined in the new interface to your existing initializers targeting v2, and -safely upgrade. (Typically, you will then have the version 2 method proxy to -the one defined in version 3.) - -The following changes were made to initializers: - -- `Laminas\ServiceManager\InitializerInterface` was renamed to - `Laminas\ServiceManager\Initializer\InitializerInterface`. -- The interface itself has a new signature. - -The previous signature was: - -```php -public function initialize($instance, ServiceLocatorInterface $serviceLocator) -``` - -It is now: - -```php -public function __invoke(ContainerInterface $container, $instance) -``` - -The changes were made to ensure the signature is internally consistent with the -various factories. - -To prepare your existing initializers for version 3, take the following steps: - -- Implement the `__invoke()` method in your existing factory, copying the code - from your existing `initialize()` method into it. -- Modify the `initialize()` method to proxy to the new method. - -As an example, consider this initializer for version 2: - -```php -use Laminas\ServiceManager\InitializerInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class FooInitializer implements InitializerInterface -{ - public function initializer($instance, ServiceLocatorInterface $services) - { - if (! $instance implements FooAwareInterface) { - return $instance; - } - $instance->setFoo($services->get(FooInterface::class); - return $instance; - } -} -``` - -To prepare this for version 3, we'd implement the `__invoke()` signature from -version 3, and modify `initialize()` to proxy to it: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\InitializerInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; - -class FooInitializer implements InitializerInterface -{ - public function __invoke(ContainerInterface $container, $instance) - { - if (! $instance implements FooAwareInterface) { - return $instance; - } - $container->setFoo($services->get(FooInterface::class); - return $instance; - } - - public function initializer($instance, ServiceLocatorInterface $services) - { - return $this($services, $instance); - } -} -``` - -After you have upgraded to version 3, you can take the following steps to remove -the migration artifacts: - -- Update your class to implement the new interface. -- Remove the `initialize()` method from your implementation. - -From our example above, we would update the class to read as follows: - -```php -use Interop\Container\ContainerInterface; -use Laminas\ServiceManager\Initializer\InitializerInterface; // <-- note the change! - -class FooInitializer implements InitializerInterface -{ - public function __invoke(ContainerInterface $container, $instance) - { - if (! $instance implements FooAwareInterface) { - return $instance; - } - $container->setFoo($services->get(FooInterface::class); - return $instance; - } -} -``` - -> ### Update your callables! -> -> Version 2 allows you to provide initializers as PHP callables. However, this -> means that the signature of those callables is incorrect for version 3! -> -> To make your code forwards compatible, you have two paths: -> -> The first is to simply provide an `InitializerInterface` implementation -> instead. This guarantees that the correct method is called based on the -> version of the `ServiceManager` in use. -> -> The second approach is to omit typehints on the arguments, and do typechecks -> internally. As an example, let's say you have the following: -> -> ```php -> $container->addInitializer(function ($instance, ContainerInterface $container) { -> if (! $instance implements FooAwareInterface) { -> return $instance; -> } -> $container->setFoo($services->get(FooInterface::class); -> return $instance; -> }); -> ``` -> -> To make this future-proof, remove the typehints, and check the types within -> the callable: -> -> ```php -> $container->addInitializer(function ($first, $second) { -> if ($first instanceof ContainerInterface) { -> $container = $first; -> $instance = $second; -> } else { -> $container = $second; -> $instance = $first; -> } -> if (! $instance implements FooAwareInterface) { -> return; -> } -> $container->setFoo($services->get(FooInterface::class); -> }); -> ``` -> -> This approach can also be done if you omitted typehints in the first place. -> Regardless, the important part to remember is that order of arguments is -> inverted between the two versions. - -## Plugin Managers - -In version 2, plugin managers were `ServiceManager` instances that implemented -both the `MutableCreationOptionsInterface` and `ServiceLocatorAwareInterface`, -and extended `AbstractPluginManager`. Plugin managers passed themselves to -factories, abstract factories, etc., requiring pulling the parent service -manager, if composed, in order to resolve application-level dependencies. - -In version 3, we define the following: - -- `Laminas\ServiceManager\PluginManagerInterface`, which provides the public API - differences from the `ServiceLocatorInterface`. -- `Laminas\ServiceManager\AbstractPluginManager`, which gives the basic - capabilities for plugin managers. The class now has a (semi) *required* - dependency on the application-level service manager instance, which is passed - to all factories, abstract factories, etc. (More on this below.) - -### PluginManagerInterface - -`Laminas\ServiceManager\PluginInterface` is a new interface for version 3, -extending `ServiceLocatorInterface` and adding one method: - -```php -/** - * Validate an instance - * - * @param object $instance - * @return void - * @throws InvalidServiceException If created instance does not respect the - * constraint on type imposed by the plugin manager - */ -public function validate($instance); -``` - -All plugin managers *must* implement this interface. For backwards-compatibility -purposes, `AbstractPluginManager` will check for the `validatePlugin()` method -(defined as abstract in v2), and, on discovery, trigger an `E_USER_DEPRECATED` -notice, followed by invocation of that method. - -### AbstractPluginManager - -As it did in version 2, `AbstractPluginManager` extends `ServiceManager`. **That -means that all changes made to the `ServiceManager` for v3 also apply to the -`AbstractPluginManager`.** - -In addition, review the following changes. - -#### Constructor - -- The constructor now accepts the following arguments, in the following order: - - The parent container instance; this is usually the application-level - `ServiceManager` instance. - - Optionally, an array of configuration for the plugin manager instance; this - should have the same format as for a `ServiceManager` instance. -- `validatePlugin()` was renamed to `validate()` (now defined in - `PluginManagerInterface`). The `AbstractPluginManager` provides - a basic implementation (detailed below). -- The signature of `get()` changes (more below). - -For backwards compatibility purposes, the constructor *also* allows the -following for the initial argument: - -- A `null` value. In this case, the plugin manager will use itself as the - creation context, *but also raise a deprecation notice indicating a - container should be passed instead.* You can pass the parent container - to the `setServiceLocator()` method to reset the creation context, but, - again, this raises a deprecation notice. -- A `ConfigInterface` instance. In this case, the plugin manager will call - the config instance's `toArray()` method to cast it to an array, and use the - return value as the configuration to pass to the parent constructor. As with - the `null` value, the plugin manager will be set as its own creation context. - -#### Validation - -The `validate()` method is defined as follows: - -```php -public function validate($instance) -{ - if (method_exists($this, 'validatePlugin')) { - trigger_error(sprintf( - '%s::validatePlugin() has been deprecated as of 3.0; please define validate() instead', - get_class($this) - ), E_USER_DEPRECATED); - $this->validatePlugin($instance); - return; - } - - if (empty($this->instanceOf) || $instance instanceof $this->instanceOf) { - return; - } - - throw new InvalidServiceException(sprintf( - 'Plugin manager "%s" expected an instance of type "%s", but "%s" was received', - __CLASS__, - $this->instanceOf, - is_object($instance) ? get_class($instance) : gettype($instance) - )); -} -``` - -The two takeaways from this are: - -- If you are upgrading from v2, your code should continue to work, *but will - emit a deprecation notice*. The way to remove the deprecation notice is to - rename the `validatePlugin()` method to `validate()`, or to remove it and - define the `$instanceOf` property (if all you're doing is checking the - plugin against a single typehint). -- Most plugin manager instances can simply define the `$instanceOf` property to - indicate what plugin interface is considered valid for the plugin manager, and - make no further changes to the abstract plugin manager: - -```php -protected $instanceOf = ValidatorInterface::class; -``` - -#### get() - -The `get()` signature changes from: - -```php -public function get($name, $options = [], $usePeeringServiceManagers = true) -``` - -to: - -```php -public function get($name, array $options = null) -``` - -Essentially: `$options` now *must* be an array if passed, and peering is no -longer supported. - -#### Deprecated methods - -Finally, the following methods from v2's `ServiceLocatorAwareInterface` are -retained (without implementing the interface), but marked as deprecated: - -- `setServiceLocator()`. This method exists as many tests and plugin manager - factories were using it to inject the parent locator (now called the creation - context). This method may still be used, and will now set the creation context - for the plugin manager, but also emit a deprecation warning. -- `getServiceLocator()` is implemented in `ServiceManager` (from which - `AbstractPluginManager` inherits), but marked as deprecated. - -Regarding this latter point, `getServiceLocator()` exists to provide backwards -compatibility *for existing plugin factories*. These factories typically pull -dependencies from the parent/application container in order to initialize the -plugin. In v2, this would look like: - -```php -function ($plugins) -{ - $services = $plugins->getServiceLocator(); - - // pull dependencies from $services: - $foo = $services->get('Foo'); - $bar = $services->get('Bar'); - - return new Plugin($foo, $bar); -} -``` - -In v3, the initial argument to the factory is not the plugin manager instance, -but the *creation context*, which is analogous to the parent locator in v2. In -order to preserve existing behavior, we added the `getServiceLocator()` method -to the `ServiceManager`. As such, the above will continue to work in v3. - -However, this method is marked as deprecated, and will emit an -`E_USER_DEPRECATED` notice. To remove the notice, you will need to upgrade your -code. The above example thus becomes: - -```php -function ($services) -{ - // pull dependencies from $services: - $foo = $services->get('Foo'); - $bar = $services->get('Bar'); - - return new Plugin($foo, $bar); -} -``` - -If you *were* using the passed plugin manager and pulling other plugins, you -will need to update your code to retrieve the plugin manager from the passed -container. As an example, given this: - -```php -function ($plugins) -{ - $anotherPlugin = $plugins->get('AnotherPlugin'); - return new Plugin($anotherPlugin); -} -``` - -You will need to rewrite it to: - -```php -function ($services) -{ - $plugins = $services->get('PluginManager'); - $anotherPlugin = $plugins->get('AnotherPlugin'); - return new Plugin($anotherPlugin); -} -``` - -### Plugin Service Creation - -The `get()` method has new behavior: - -- When non-empty `$options` are passed, it *always* delegates to `build()`, and - thus will *always* return a *new instance*. If you are using `$options`, the - assumption is that you are using the plugin manager as a factory, and thus the - instance should not be cached. -- Without `$options`, `get()` will cache by default (the default behavior of - `ServiceManager`). To *never* cache instances, either set the - `$sharedByDefault` class property to `false`, or pass a boolean `false` value - via the `shared_by_default` configuration key. - -### Migration example - -Let's consider the following plugin manager geared towards version 2: - -```php -use RuntimeException; -use Laminas\ServiceManager\AbstractPluginManager; - -class ObserverPluginManager extends AbstractPluginManager -{ - protected $invokables = [ - 'mail' => MailObserver::class, - 'log' => LogObserver::class, - ]; - - protected $shareByDefault = false; - - public function validatePlugin($instance) - { - if (! $instance instanceof ObserverInterface) { - throw new RuntimeException(sprintf( - 'Invalid plugin "%s" created; not an instance of %s', - get_class($instance), - ObserverInterface::class - )); - } - } -} -``` - -To prepare this for version 3, we need to do the following: - -- We need to change the `$invokables` configuration to a combination of - `factories` and `aliases`. -- We need to implement a `validate()` method. -- We need to update the `validatePlugin()` method to proxy to `validate()`. -- We need to add a `$sharedByDefault` property (if `$shareByDefault` is present). - -Doing so, we get the following result: - -```php -namespace MyNamespace; - -use RuntimeException; -use Laminas\ServiceManager\AbstractPluginManager; -use Laminas\ServiceManager\Exception\InvalidServiceException; -use Laminas\ServiceManager\Factory\InvokableFactory; - -class ObserverPluginManager extends AbstractPluginManager -{ - protected $instanceOf = ObserverInterface::class; - - protected $aliases = [ - 'mail' => MailObserver::class, - 'Mail' => MailObserver::class, - 'log' => LogObserver::class, - 'Log' => LogObserver::class, - ]; - - protected $factories = [ - MailObserver::class => InvokableFactory::class, - LogObserver::class => InvokableFactory::class, - // Legacy (v2) due to alias resolution - 'mynamespacemailobserver' => InvokableFactory::class, - 'mynamespacelogobserver' => InvokableFactory::class, - ]; - - protected $shareByDefault = false; - - protected $sharedByDefault = false; - - public function validate($instance) - { - if (! $instance instanceof $this->instanceOf) { - throw new InvalidServiceException(sprintf( - 'Invalid plugin "%s" created; not an instance of %s', - get_class($instance), - $this->instanceOf - )); - } - } - - public function validatePlugin($instance) - { - try { - $this->validate($instance); - } catch (InvalidServiceException $e) { - throw new RuntimeException($e->getMessage(), $e->getCode(), $e); - } - } -} -``` - -Things to note about the above: - -- It introduces a new property, `$instanceOf`. We'll use this later, when we're - ready to clean up post-migration. -- It introduces four aliases. This is to allow fetching the various plugins as - any of `mail`, `Mail`, `log`, or `Log` — all of which are valid in - version 2, but, because version 3 does not normalize names, need to be - explicitly aliased. -- The aliases point to the fully qualified class name (FQCN) for the service - being generated, and these are mapped to `InvokableFactory` instances. This - means you can also fetch your plugins by their FQCN. -- There are also factory entries for the canonicalized FQCN of each factory, - which will be used in v2. (Canonicalization in v2 strips non-alphanumeric - characters, and casts to lowercase.) -- `validatePlugin()` continues to throw the old exception - -The above will now work in both version 2 and version 3. - -### Migration testing - -To test your changes, create a new `MigrationTest` case that uses -`Laminas\ServiceManager\Test\CommonPluginManagerTrait`. Override -`getPluginManager()` to return an instance of your plugin manager, and override -`getV2InvalidPluginException()` to return the classname of the exception your -`validatePlugin()` method throws: - -```php -use MyNamespace\ObserverInterface; -use MyNamespace\ObserverPluginManager; -use MyNamespace\Exception\RuntimeException; -use PHPUnit_Framework_TestCase as TestCase; -use Laminas\ServiceManager\ServiceManager; -use Laminas\ServiceManager\Test\CommonPluginManagerTrait; - -class MigrationTest extends TestCase -{ - use CommonPluginManagerTrait; - - protected function getPluginManager() - { - return new ObserverPluginManager(new ServiceManager()); - } - - protected function getV2InvalidPluginException() - { - return RuntimeException::class; - } - - protected function getInstanceOf() - { - return ObserverInterface::class; - } -} -``` - -This will check that: - -- You have set the `$instanceOf` property. -- `$shareByDefault` and `$sharedByDefault` match, if present. -- That requesting an invalid plugin throws the right exception. -- That all your aliases resolve. - - -### Post migration - -After you migrate to version 3, you can clean up your plugin manager: - -- Remove the `validatePlugin()` method. -- If your `validate()` routine is only checking that the instance is of a single - type, and has no other logic, you can remove that implementation as well, as - the `AbstractPluginManager` already takes care of that when `$instanceOf` is - defined! -- Remove the canonicalized FQCN entry for each factory - -Performing these steps on the above, we get: - -```php -use Laminas\ServiceManager\AbstractPluginManager; -use Laminas\ServiceManager\Factory\InvokableFactory; - -class ObserverPluginManager extends AbstractPluginManager -{ - protected $instanceOf = ObserverInterface::class; - - protected $aliases = [ - 'mail' => MailObserver::class, - 'Mail' => MailObserver::class, - 'log' => LogObserver::class, - 'Log' => LogObserver::class, - ]; - - protected $factories = [ - MailObserver::class => InvokableFactory::class, - LogObserver::class => InvokableFactory::class, - ]; -} -``` - -## DI Namespace - -**The `Laminas\ServiceManager\Di` namespace has been removed.** - -The `Laminas\Di` component is not actively maintained, and has been largely -deprecated during the Laminas lifecycle in favor of the Service Manager. Its usage -as an abstract factory is problematic and error prone when used in conjunction -with the Service Manager; as such, we've removed it for the initial v3 release. - -We may re-introduce it via a separate component in the future. - -## Miscellaneous Interfaces, Traits, and Classes - -The following interfaces, traits, and classes were *removed*: - -- `Laminas\ServiceManager\MutableCreationOptionsInterface`; this was previously - used by the `AbstractPluginManager`, and is no longer required as we ship a - separate `PluginManagerInterface`, and because the functionality is - encompassed by the `build()` method. -- `Laminas\ServiceManager\MutableCreationOptionsTrait` -- `Laminas\ServiceManager\Proxy\LazyServiceFactoryFactory`; its capabilities were - moved directly into the `ServiceManager`. -- `Laminas\ServiceManager\ServiceLocatorAwareInterface` -- `Laminas\ServiceManager\ServiceLocatorAwareTrait` -- `Laminas\ServiceManager\ServiceManagerAwareInterface` - -The `ServiceLocatorAware` and `ServiceManagerAware` interfaces and traits were -too often abused under v2, and represent the antithesis of the purpose of the -Service Manager component; dependencies should be directly injected, and the -container should never be composed by objects. - -The following classes and interfaces have changes: - -- `Laminas\ServiceManager\Proxy\LazyServiceFactory` is now marked `final`, and - implements `Laminas\ServiceManager\Proxy\DelegatorFactoryInterface`. Its - dependencies and capabilities remain the same. -- `Laminas\ServiceManager\ConfigInterface` now is expected to *return* the modified - `ServiceManager` instance. -- `Laminas\ServiceManager\Config` was updated to follow the changes to - `ConfigInterface` and `ServiceManager`, and now returns the updated - `ServiceManager` instance from `configureServiceManager()`. +TBD From f0e62cb4593c4fd880ba858975e8651824745702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:43:57 +0200 Subject: [PATCH 13/32] docs: normalize ahead-of-time-factories.md to match markdown rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/ahead-of-time-factories.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/book/v4/ahead-of-time-factories.md b/docs/book/v4/ahead-of-time-factories.md index ccf34d98..3afe46ad 100644 --- a/docs/book/v4/ahead-of-time-factories.md +++ b/docs/book/v4/ahead-of-time-factories.md @@ -1,10 +1,10 @@ -# Ahead of Time Factories +# Ahead of Time Factories - Since 4.0.0 In addition to the already existing [Reflection Factory](reflection-abstract-factory.md), one can create factories for those services using `ReflectionBasedAbstractFactory` before deploying the project to production. 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. +Using `ReflectionBasedAbstractFactory` in production is not recommended as the usage of `Reflection` is not too performant. ## Usage @@ -12,7 +12,7 @@ It is recommended to create factories within CI pipeline. While developing a ser To generate the factories, run the following CLI command after [setting up the project](#project-setup): -``` +```shell $ php vendor/bin/laminas servicemanager:generate-aot-factories [] ``` @@ -23,7 +23,7 @@ When the CLI command has finished, there are all factories generated within the 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 `` 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. +Ensure that both `` 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 @@ -34,7 +34,7 @@ The project needs some additional configuration so that the generated factories 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. -``` +```shell $ composer require --dev laminas/laminas-cli ``` @@ -46,7 +46,7 @@ Use either `config/autoload/global.php` (which might already exist) or the `Appl Both Laminas-MVC and Mezzio do share the configuration directory structure as follows: -``` +```text . ├── config │   ├── autoload @@ -80,6 +80,7 @@ This will provide composer with the information, that PHP classes can be found w > The `autoload` config folder is scanned for files named `[].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 From 32d6cc9d418e28b18c5f38a95be73ebb0511f4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 8 Apr 2023 02:45:34 +0200 Subject: [PATCH 14/32] docs: remove trailing punctuation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v3/migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/v3/migration.md b/docs/book/v3/migration.md index 482b2374..8fa1d899 100644 --- a/docs/book/v3/migration.md +++ b/docs/book/v3/migration.md @@ -855,7 +855,7 @@ class FooInitializer implements InitializerInterface } ``` -> ### Update your callables! +> ### Update your callables > > Version 2 allows you to provide initializers as PHP callables. However, this > means that the signature of those callables is incorrect for version 3! From 178c011c5d5fbad7b8036b4629dc50e33e19e50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:03:54 +0200 Subject: [PATCH 15/32] qa: mark `AheadOfTimeCompiledFactory#__construct` as internal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Tool/AheadOfTimeCompiledFactory.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tool/AheadOfTimeCompiledFactory.php b/src/Tool/AheadOfTimeCompiledFactory.php index bc2f5e64..df32e541 100644 --- a/src/Tool/AheadOfTimeCompiledFactory.php +++ b/src/Tool/AheadOfTimeCompiledFactory.php @@ -7,6 +7,8 @@ final class AheadOfTimeCompiledFactory { /** + * @internal + * * @param class-string $fullyQualifiedClassName * @param non-empty-string $containerConfigurationKey * @param non-empty-string $generatedFactory From c0775662048c19966599619b1d1ae1ef3ac92636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:04:34 +0200 Subject: [PATCH 16/32] qa: verify that `localConfigFilename` is actually writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Command/AheadOfTimeFactoryCreatorCommand.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index ca4475ff..628fa115 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -17,6 +17,7 @@ use function assert; use function count; +use function dirname; use function file_put_contents; use function is_dir; use function is_string; @@ -64,7 +65,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf( 'Please configure the `%s` configuration key in your projects config and ensure that the' . ' directory is registered to the composer autoloader using `classmap` and writable by the executing' - . ' user.', + . ' user. In case you are targeting a nonexistent directory, please create the appropriate directory' + . ' structure before executing this command.', ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH, )); @@ -74,6 +76,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $localConfigFilename = $input->getArgument('localConfigFilename'); assert(is_string($localConfigFilename)); + if (! is_writable(dirname($localConfigFilename))) { + $output->writeln(sprintf( + 'Provided `localConfigFilename` argument "%s" is not writable. In case you are targeting a' + . ' nonexistent directory, please create the appropriate directory structure before executing this' + . ' command.', + $localConfigFilename, + )); + + return self::FAILURE; + } + $compiledFactories = $this->factoryCompiler->compile($this->config); if ($compiledFactories === []) { $output->writeln( From be9edd7133b7277533568e08cea148d91b99ef22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:04:46 +0200 Subject: [PATCH 17/32] qa: only allow array configuration to be passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Command/AheadOfTimeFactoryCreatorCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index 628fa115..93397527 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -37,7 +37,7 @@ final class AheadOfTimeFactoryCreatorCommand extends Command public const NAME = 'servicemanager:generate-aot-factories'; public function __construct( - private iterable $config, + private array $config, private string $factoryTargetPath, private AheadOfTimeFactoryCompilerInterface $factoryCompiler, ) { From eb58addf3b0661e819c33176c8434387b25e1b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:05:01 +0200 Subject: [PATCH 18/32] qa: add unit tests for `AheadOfTimeFactoryCreatorCommand` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../AheadOfTimeFactoryCreatorCommandTest.php | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/Command/AheadOfTimeFactoryCreatorCommandTest.php diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php new file mode 100644 index 00000000..6382f89e --- /dev/null +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -0,0 +1,250 @@ +input = $this->createMock(InputInterface::class); + $this->output = $this->createMock(OutputInterface::class); + $this->factoryTargetPath = vfsStream::setup('root', 0644); + $this->factoryCompiler = $this->createMock(AheadOfTimeFactoryCompilerInterface::class); + } + + /** + * @return array + */ + public function invalidFactoryTargetPaths(): array + { + $readOnlyDirectory = vfsStream::setup('read-only', 0544, ['bar' => []]); + return [ + 'no target path' => [''], + 'read-only directory' => [$readOnlyDirectory->getChild('bar')->url()], + 'nonexistent-directory' => ['/foo/bar/baz'], + ]; + } + + /** + * @dataProvider invalidFactoryTargetPaths + */ + public function testEmitsErrorMessageIfFactoryTargetPathDoesNotMatchRequirements(string $factoryTargetPath): void + { + $command = new AheadOfTimeFactoryCreatorCommand([], $factoryTargetPath, $this->factoryCompiler); + + $this->factoryCompiler + ->expects(self::never()) + ->method(self::anything()); + + $this->assertErrorRaised(sprintf( + 'Please configure the `%s` configuration key in your projects config and ensure that the' + . ' directory is registered to the composer autoloader using `classmap` and writable by the executing' + . ' user. In case you are targeting a nonexistent directory, please create the appropriate directory' + . ' structure before executing this command.', + ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH + )); + self::assertSame(1, $command->run($this->input, $this->output)); + } + + public function assertErrorRaised(string $message): void + { + $this->output + ->expects(self::once()) + ->method('writeln') + ->with(self::stringContains(sprintf('%s', $message))); + } + + public function testWillNotCreateConfigurationFileWhenNoFactoriesDetected(): void + { + $directory = $this->factoryTargetPath->url(); + + $command = new AheadOfTimeFactoryCreatorCommand( + [], + $directory, + $this->factoryCompiler, + ); + + $this->input + ->method('getArgument') + ->with('localConfigFilename') + ->willReturn(sprintf('%s/generated-factories.local.php', $directory)); + + $this->output + ->expects(self::once()) + ->method('writeln') + ->with( + 'There is no (more) service registered to use the `ReflectionBasedAbstractFactory`.' + ); + + $this->factoryCompiler + ->expects(self::once()) + ->method('compile') + ->willReturn([]); + + $command->run($this->input, $this->output); + + self::assertCount(0, $this->factoryTargetPath->getChildren()); + } + + /** + * @requires testWillVerifyLocalConfigFilenameIsWritable + */ + public function testWillCreateExpectedGeneratedFactoriesConfig(): void + { + $directory = $this->factoryTargetPath->url(); + + $command = new AheadOfTimeFactoryCreatorCommand( + [], + $directory, + $this->factoryCompiler, + ); + + $localConfigFilename = 'yada-yada.local.php'; + + $this->input + ->method('getArgument') + ->with('localConfigFilename') + ->willReturn(sprintf('%s/%s', $directory, $localConfigFilename)); + + $generatedFactory = file_get_contents(__DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'); + assert($generatedFactory !== ''); + + $this->factoryCompiler + ->expects(self::once()) + ->method('compile') + ->willReturn([ + new AheadOfTimeCompiledFactory( + SimpleDependencyObject::class, + 'foobar', + $generatedFactory, + ), + ]); + + $this->output + ->expects(self::once()) + ->method('writeln') + ->with('Successfully created 1 factories.'); + + $command->run($this->input, $this->output); + + self::assertCount(2, $this->factoryTargetPath->getChildren()); + self::assertTrue($this->factoryTargetPath->hasChild('foobar')); + $foobarDirectory = $this->factoryTargetPath->getChild('foobar'); + self::assertInstanceOf(vfsStreamDirectory::class, $foobarDirectory); + self::assertTrue($foobarDirectory->hasChild( + 'LaminasTest_ServiceManager_TestAsset_SimpleDependencyObjectFactory.php' + )); + $generatedFactoryFile = $foobarDirectory->getChild( + 'LaminasTest_ServiceManager_TestAsset_SimpleDependencyObjectFactory.php' + ); + self::assertInstanceOf(vfsStreamFile::class, $generatedFactoryFile); + self::assertSame($generatedFactory, $generatedFactoryFile->getContent()); + self::assertTrue($this->factoryTargetPath->hasChild('yada-yada.local.php')); + $localConfigFile = $this->factoryTargetPath->getChild('yada-yada.local.php'); + self::assertInstanceOf(vfsStreamFile::class, $localConfigFile); + /** @psalm-suppress UnresolvableInclude Psalm is unable to determine i/o when using vfs stream wrapper */ + $localConfiguration = require $localConfigFile->url(); + self::assertIsArray($localConfiguration, 'Expected generated local config file to return an array.'); + self::assertArrayHasKey( + 'foobar', + $localConfiguration, + 'Expected local configuration containing an array key `foobar`' + ); + $localFoobarServiceManagerConfiguration = $localConfiguration['foobar']; + self::assertIsArray( + $localFoobarServiceManagerConfiguration, + 'Expected local configuration `foobar` key provides an array structure' + ); + self::assertArrayHasKey( + 'factories', + $localFoobarServiceManagerConfiguration, + 'Expected local configuration `foobar` key provides an array structure with a `factories` key.' + ); + $localFoobarServiceManagerFactories = $localFoobarServiceManagerConfiguration['factories']; + self::assertIsArray( + $localFoobarServiceManagerFactories, + 'Expected local configuration `foobar` key provides a factory map.' + ); + self::assertArrayHasKey( + SimpleDependencyObject::class, + $localFoobarServiceManagerFactories, + sprintf( + 'Expected local configuration `foobar` factory map provides a factory for "%s".', + SimpleDependencyObject::class, + ), + ); + + self::assertSame( + sprintf('%sFactory', SimpleDependencyObject::class), + $localFoobarServiceManagerFactories[SimpleDependencyObject::class], + ); + } + + public function testWillVerifyLocalConfigFilenameIsWritable(): void + { + $localConfigFilename = sprintf('foo/bar/baz/qoo/ooq/%s', 'yada-yada.local.php'); + + $directory = $this->factoryTargetPath->url(); + + $command = new AheadOfTimeFactoryCreatorCommand( + [], + $directory, + $this->factoryCompiler, + ); + + $localConfigPath = sprintf('%s/%s', $directory, $localConfigFilename); + + $this->input + ->method('getArgument') + ->with('localConfigFilename') + ->willReturn($localConfigPath); + + $this->factoryCompiler + ->expects(self::never()) + ->method(self::anything()); + + $this->assertErrorRaised(sprintf( + 'Provided `localConfigFilename` argument "%s" is not writable. In case you are targeting a' + . ' nonexistent directory, please create the appropriate directory structure before executing this' + . ' command.', + $localConfigPath, + )); + + $command->run($this->input, $this->output); + } +} From 227eb726948c78d96d86c3f6de93bc588a3d8ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:33:40 +0200 Subject: [PATCH 19/32] qa: modify config type to `array` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Tool/AheadOfTimeFactoryCompiler.php | 12 +++++------- src/Tool/AheadOfTimeFactoryCompilerInterface.php | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Tool/AheadOfTimeFactoryCompiler.php b/src/Tool/AheadOfTimeFactoryCompiler.php index 2bb83cfd..6f977c16 100644 --- a/src/Tool/AheadOfTimeFactoryCompiler.php +++ b/src/Tool/AheadOfTimeFactoryCompiler.php @@ -24,7 +24,7 @@ public function __construct( ) { } - public function compile(iterable $config): array + public function compile(array $config): array { $servicesRegisteredByReflectionBasedFactory = $this->extractServicesRegisteredByReflectionBasedFactory( $config @@ -44,15 +44,14 @@ public function compile(iterable $config): array } /** - * @param iterable $config * @return array}> */ - private function extractServicesRegisteredByReflectionBasedFactory(iterable $config): array + private function extractServicesRegisteredByReflectionBasedFactory(array $config): array { $services = []; foreach ($config as $key => $entry) { - if (! is_array($entry)) { + if (! is_string($key) || $key === '' || ! is_array($entry)) { continue; } @@ -73,12 +72,11 @@ private function extractServicesRegisteredByReflectionBasedFactory(iterable $con continue; } - assert(is_string($key) && $key !== ''); - foreach ($servicesUsingReflectionBasedFactory as $service => $factory) { if (! class_exists($service)) { throw new InvalidArgumentException(sprintf( - 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist.', + 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist or does' + .' not refer to an actual class.', $service )); } diff --git a/src/Tool/AheadOfTimeFactoryCompilerInterface.php b/src/Tool/AheadOfTimeFactoryCompilerInterface.php index 4811ca7e..16d807bc 100644 --- a/src/Tool/AheadOfTimeFactoryCompilerInterface.php +++ b/src/Tool/AheadOfTimeFactoryCompilerInterface.php @@ -9,5 +9,5 @@ interface AheadOfTimeFactoryCompilerInterface /** * @return list */ - public function compile(iterable $config): array; + public function compile(array $config): array; } From 766b4702ab9da7d3cf0c395975265bd4d82de192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 01:35:57 +0200 Subject: [PATCH 20/32] docs: add dedicated console documentation for the ahead of time factories CLI command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/ahead-of-time-factories.md | 4 +- docs/book/v4/console-tools.md | 90 ++++++++++------ docs/book/v4/reflection-abstract-factory.md | 109 ++------------------ 3 files changed, 66 insertions(+), 137 deletions(-) diff --git a/docs/book/v4/ahead-of-time-factories.md b/docs/book/v4/ahead-of-time-factories.md index 3afe46ad..24fc8ffb 100644 --- a/docs/book/v4/ahead-of-time-factories.md +++ b/docs/book/v4/ahead-of-time-factories.md @@ -2,9 +2,7 @@ - Since 4.0.0 -In addition to the already existing [Reflection Factory](reflection-abstract-factory.md), one can create factories for those services using `ReflectionBasedAbstractFactory` before deploying the project to production. -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. +In addition to the already existing [Reflection Factory](reflection-abstract-factory.md), one can create factories for those services using `ReflectionBasedAbstractFactory` before deploying the project to production. For the initial project setup regarding CLI tooling, please refer to [this documentation](console-tools.md#requirements). ## Usage diff --git a/docs/book/v4/console-tools.md b/docs/book/v4/console-tools.md index d54de33a..76689519 100644 --- a/docs/book/v4/console-tools.md +++ b/docs/book/v4/console-tools.md @@ -1,32 +1,41 @@ # Console Tools -Starting in 3.2.0, laminas-servicemanager began shipping with console tools. This -document details each. +Starting in 4.0.0, `laminas-servicemanager` moved the CLI tooling to `laminas-cli` and provides several commands to be executed. -## generate-deps-for-config-factory +## Requirements -```bash -$ ./vendor/bin/generate-deps-for-config-factory -Usage: +To run the console tools with `laminas-servicemanager` v4, the [`laminas/laminas-cli`](https://docs.laminas.dev/laminas-cli/) component needs to be added to the project dependencies. - generate-deps-for-config-factory [-h|--help|help] [-i|--ignore-unresolved] +> ### Installation +> +> ```shell +> $ composer require laminas/laminas-cli +> ``` +> _In case laminas-cli is only required to consume these console tools, you might consider using the `--dev` flag._ -Arguments: +## Available Commands - -h|--help|help This usage message - -i|--ignore-unresolved Ignore classes with unresolved direct dependencies. - Path to a config file for which to generate - configuration. If the file does not exist, it will - be created. If it does exist, it must return an - array, and the file will be updated with new - configuration. - Name of the class to reflect and for which to - generate dependency configuration. +- [Generate Dependencies for Config Factory](#generate-dependencies-for-config-factory) +- [Generate Factory for Class](#generate-factory-for-class) +- [Generate Ahead of Time Factories](#ahead-of-time-factories) +## Generate Dependencies for Config Factory -Reads the provided configuration file (creating it if it does not exist), -and injects it with ConfigAbstractFactory dependency configuration for -the provided class name, writing the changes back to the file. +```bash +$ ./vendor/bin/laminas servicemanager:generate-deps-for-config-factory -h +Description: + Reads the provided configuration file (creating it if it does not exist), and injects it with ConfigAbstractFactory dependency configuration for the provided class name, writing the changes back to the file. + +Usage: + servicemanager:generate-deps-for-config-factory [options] [--] + +Arguments: + configFile Path to a config file for which to generate configuration. If the file does not exist, it will be created. If it does exist, it must return an array, and the file will be updated with new configuration. + class Name of the class to reflect and for which to generate dependency configuration. + +Options: + -i, --ignore-unresolved Ignore classes with unresolved direct dependencies. + -q, --quiet Do not output any message ``` This utility will generate dependency configuration for the named class for use @@ -42,23 +51,21 @@ exception message. By adding the flag, you can have it continue and produce configuration. This option is particularly useful when typehints are on interfaces or resolve to services served by other abstract factories. -## generate-factory-for-class +## Generate Factory for Class ```bash -$ ./vendor/bin/generate-factory-for-class +$ ./vendor/bin/laminas servicemanager:generate-factory-for-class -h +Description: + Generates to STDOUT a factory for creating the specified class; this may then be added to your application, and configured as a factory for the class. Usage: - - ./bin/generate-factory-for-class [-h|--help|help] + servicemanager:generate-factory-for-class Arguments: + className Name of the class to reflect and for which to generate a factory. - -h|--help|help This usage message - Name of the class to reflect and for which to generate - a factory. - -Generates to STDOUT a factory for creating the specified class; this may then -be added to your application, and configured as a factory for the class. +Options: + -q, --quiet Do not output any message ``` This utility generates a factory class for the given class, based on the @@ -66,9 +73,30 @@ typehints in its constructor. The factory is emitted to STDOUT, and may be piped to a file if desired: ```bash -$ ./vendor/bin/generate-factory-for-class \ +$ ./vendor/bin/laminas servicemanager:generate-factory-for-class \ > "Application\\Model\\AlbumModel" > ./module/Application/src/Model/AlbumModelFactory.php ``` The class generated implements `Laminas\ServiceManager\Factory\FactoryInterface`, and is generated within the same namespace as the originating class. + +## Generate Ahead of Time Factories + +```bash +$ vendor/bin/laminas servicemanager:generate-aot-factories -h +Description: + Creates factories which replace the runtime overhead for `ReflectionBasedAbstractFactory`. + +Usage: + servicemanager:generate-aot-factories [] + +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"] + +Options: + -q, --quiet Do not output any message +``` + +This utility will generate factories in the same way as [servicemanager:generate-factory-for-class](#generate-factory-for-class). The main difference is, that it will scan the whole project configuration for the usage of `ReflectionBasedAbstractFactory` within **any** ServiceManager look-a-like configuration (i.e. explicit usage within `factories`) and auto-generates factories for all of these services **plus** creates a configuration file which overrides **all** ServiceManager look-a-like configurations so that these consume the generated factories. + +For more details and how to set up a project so that all factories are properly replaced, refer to the [dedicated command documentation](ahead-of-time-factories.md). diff --git a/docs/book/v4/reflection-abstract-factory.md b/docs/book/v4/reflection-abstract-factory.md index 9e771b6d..cbd9beeb 100644 --- a/docs/book/v4/reflection-abstract-factory.md +++ b/docs/book/v4/reflection-abstract-factory.md @@ -28,7 +28,7 @@ return [ ]; ``` -Mapping services to the factory is more explicit and performant. +Mapping services to the factory is more explicit and even more performant than in v3.0 due to the [ahead of time factory generation](ahead-of-time-factories.md). The factory operates with the following constraints/features: @@ -44,110 +44,13 @@ The factory operates with the following constraints/features: `$options` passed to the factory are ignored in all cases, as we cannot make assumptions about which argument(s) they might replace. -Once your dependencies have stabilized, we recommend writing a dedicated -factory, as reflection can introduce performance overhead; you may use the -[generate-factory-for-class console tool](console-tools.md#generate-factory-for-class) -to do so. +Once your dependencies have stabilized, we recommend providing a dedicated +factory, as reflection introduces a performance overhead. -## Handling well-known services +There are two ways to provide dedicated factories for services consuming `ReflectionBasedAbstractFactory`: -Some services provided by Laminas components do not have -entries based on their class name (for historical reasons). As examples: - -- `Laminas\Console\Adapter\AdapterInterface` maps to the service name `ConsoleAdapter`, -- `Laminas\Filter\FilterPluginManager` maps to the service name `FilterManager`, -- `Laminas\Hydrator\HydratorPluginManager` maps to the service name `HydratorManager`, -- `Laminas\InputFilter\InputFilterPluginManager` maps to the service name `InputFilterManager`, -- `Laminas\Log\FilterPluginManager` maps to the service name `LogFilterManager`, -- `Laminas\Log\FormatterPluginManager` maps to the service name `LogFormatterManager`, -- `Laminas\Log\ProcessorPluginManager` maps to the service name `LogProcessorManager`, -- `Laminas\Log\WriterPluginManager` maps to the service name `LogWriterManager`, -- `Laminas\Serializer\AdapterPluginManager` maps to the service name `SerializerAdapterManager`, -- `Laminas\Validator\ValidatorPluginManager` maps to the service name `ValidatorManager`, - -To allow the `ReflectionBasedAbstractFactory` to find these, you have two -options. - -The first is to pass an array of mappings via the constructor: - -```php -$reflectionFactory = new ReflectionBasedAbstractFactory([ - \Laminas\Console\Adapter\AdapterInterface::class => 'ConsoleAdapter', - \Laminas\Filter\FilterPluginManager::class => 'FilterManager', - \Laminas\Hydrator\HydratorPluginManager::class => 'HydratorManager', - \Laminas\InputFilter\InputFilterPluginManager::class => 'InputFilterManager', - \Laminas\Log\FilterPluginManager::class => 'LogFilterManager', - \Laminas\Log\FormatterPluginManager::class => 'LogFormatterManager', - \Laminas\Log\ProcessorPluginManager::class => 'LogProcessorManager', - \Laminas\Log\WriterPluginManager::class => 'LogWriterManager', - \Laminas\Serializer\AdapterPluginManager::class => 'SerializerAdapterManager', - \Laminas\Validator\ValidatorPluginManager::class => 'ValidatorManager', -]); -``` - -This can be done either in your configuration file (which could be problematic -when considering serialization for caching), or during an early phase of -application bootstrapping. - -For instance, with laminas-mvc, this might be in your `Application` module's -bootstrap listener: - -```php -namespace Application - -use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; - -class Module -{ - public function onBootstrap($e) - { - $application = $e->getApplication(); - $container = $application->getServiceManager(); - - $container->addAbstractFactory(new ReflectionBasedAbstractFactory([ - /* ... */ - ])); - } -} -``` - -For Mezzio, it could be part of your `config/container.php` definition: - -```php -$container = new ServiceManager(); -(new Config($config['dependencies']))->configureServiceManager($container); -// Add the following: -$container->addAbstractFactory(new ReflectionBasedAbstractFactory([ - /* ... */ -])); -``` - -The second approach is to extend the class, and define the map in the -`$aliases` property: - -```php -namespace Application; - -use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; - -class ReflectionAbstractFactory extends ReflectionBasedAbstractFactory -{ - protected $aliases = [ - \Laminas\Console\Adapter\AdapterInterface::class => 'ConsoleAdapter', - \Laminas\Filter\FilterPluginManager::class => 'FilterManager', - \Laminas\Hydrator\HydratorPluginManager::class => 'HydratorManager', - \Laminas\InputFilter\InputFilterPluginManager::class => 'InputFilterManager', - \Laminas\Log\FilterPluginManager::class => 'LogFilterManager', - \Laminas\Log\FormatterPluginManager::class => 'LogFormatterManager', - \Laminas\Log\ProcessorPluginManager::class => 'LogProcessorManager', - \Laminas\Log\WriterPluginManager::class => 'LogWriterManager', - \Laminas\Serializer\AdapterPluginManager::class => 'SerializerAdapterManager', - \Laminas\Validator\ValidatorPluginManager::class => 'ValidatorManager', - ]; -} -``` - -You could then register it via class name in your service configuration. +1. Usage of the [generate-factory-for-class console tool](console-tools.md#generate-factory-for-class) (this will also require to manually modify the configuration) +2. Usage of the [generate-aot-factories console tool](console-tools.md#generate-ahead-of-time-factories) which needs an initial project + deployment setup ## Alternatives From 4ef01910f7b2e89b141444ff857686f9b9b95a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 02:52:11 +0200 Subject: [PATCH 21/32] refactor: early return invokable when there are no required parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Tool/ConfigDumper.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tool/ConfigDumper.php b/src/Tool/ConfigDumper.php index 20f5b32d..4e131c2e 100644 --- a/src/Tool/ConfigDumper.php +++ b/src/Tool/ConfigDumper.php @@ -59,17 +59,16 @@ public function createDependencyConfig(array $config, string $className, bool $i return $this->createInvokable($config, $className); } - $constructorArguments = $constructor->getParameters(); - $constructorArguments = array_filter( - $constructorArguments, - static fn(ReflectionParameter $argument): bool => ! $argument->isOptional() - ); - // has no required parameters, treat it as an invokable - if ($constructorArguments === []) { + if ($constructor->getNumberOfRequiredParameters() === 0) { return $this->createInvokable($config, $className); } + $constructorArguments = array_filter( + $constructor->getParameters(), + static fn(ReflectionParameter $argument): bool => ! $argument->isOptional() + ); + $classConfig = []; foreach ($constructorArguments as $constructorArgument) { @@ -81,6 +80,7 @@ public function createDependencyConfig(array $config, string $className, bool $i // don't throw an exception, just return the previous config return $config; } + // don't throw an exception if the class is an already defined service if ($this->container && $this->container->has($className)) { return $config; From b949debd0e1b01aaced9218fb4c81c8c8a48690d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 02:52:42 +0200 Subject: [PATCH 22/32] qa: apply coding standard to `AheadOfTimeFactoryCompiler` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Tool/AheadOfTimeFactoryCompiler.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tool/AheadOfTimeFactoryCompiler.php b/src/Tool/AheadOfTimeFactoryCompiler.php index 6f977c16..2fa04ae9 100644 --- a/src/Tool/AheadOfTimeFactoryCompiler.php +++ b/src/Tool/AheadOfTimeFactoryCompiler.php @@ -9,7 +9,6 @@ use function array_filter; use function array_key_exists; -use function assert; use function class_exists; use function is_array; use function is_string; @@ -76,7 +75,7 @@ private function extractServicesRegisteredByReflectionBasedFactory(array $config if (! class_exists($service)) { throw new InvalidArgumentException(sprintf( 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist or does' - .' not refer to an actual class.', + . ' not refer to an actual class.', $service )); } From da333d7a888d4cf87a029adbf659d79ab7bdc9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 02:53:59 +0200 Subject: [PATCH 23/32] refactor: move unit tests from `ReflectionBasedAbstractFactory` to `ConstructorParameterResolver` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 <2189546+boesing@users.noreply.github.com> --- psalm-baseline.xml | 69 +--- src/Tool/ConstructorParameterResolver.php | 15 +- .../ConstructorParameterResolverInterface.php | 4 +- .../ReflectionBasedAbstractFactoryTest.php | 274 +++++----------- ...assWithConstructorAcceptingAnyArgument.php | 16 + ...> ClassWithTypehintedDefaultNullValue.php} | 2 +- ...thConstructorWithOnlyOptionalArguments.php | 18 + .../Tool/ConstructorParameterResolverTest.php | 307 ++++++++++++++++++ 8 files changed, 429 insertions(+), 276 deletions(-) create mode 100644 test/AbstractFactory/TestAsset/ClassWithConstructorAcceptingAnyArgument.php rename test/AbstractFactory/TestAsset/{ClassWithTypehintedDefaultValue.php => ClassWithTypehintedDefaultNullValue.php} (84%) create mode 100644 test/TestAsset/ClassWithConstructorWithOnlyOptionalArguments.php create mode 100644 test/Tool/ConstructorParameterResolverTest.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index bcc795db..562d878a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -20,36 +20,9 @@ $requestedName - - new $requestedName() - new $requestedName() - new $requestedName(...$parameters) - - - function (ReflectionParameter $parameter) use ($container, $requestedName) { - - - $requestedName - - - $requestedName - $requestedName - $requestedName - - - new $requestedName() - new $requestedName() + new $requestedName(...$parameters) - - DispatchableInterface - - - is_string($type) - - - DispatchableInterface - @@ -279,21 +252,6 @@ 'Holistic' - - - assertInstanceOf - assertInstanceOf - assertInstanceOf - assertInstanceOf - assertInstanceOf - assertInstanceOf - assertInstanceOf - assertInstanceOf - - - array - - setServiceLocator @@ -334,9 +292,6 @@ - - getServiceLocator - $object['get'][0] @@ -368,7 +323,7 @@ $names[$name] $object[$shared ? $method : 'build'] - + $first $idx1 $idx2 @@ -379,14 +334,6 @@ $nonSharedObj1 $nonSharedObj2 $obj - $object1 - $object1 - $object1 - $object1 - $object2 - $object2 - $object2 - $object2 $object[$shared ? $method : 'build'][] $second $shared @@ -467,15 +414,9 @@ $context $name - - $a - $alias - $alias - $b - $headAlias + $inc $inc - $instance $instance1 $instance1 $instance2 @@ -492,10 +433,6 @@ $inc - - $instance->foo - $instance->option - $container $container diff --git a/src/Tool/ConstructorParameterResolver.php b/src/Tool/ConstructorParameterResolver.php index 9e00fca7..3ce5500f 100644 --- a/src/Tool/ConstructorParameterResolver.php +++ b/src/Tool/ConstructorParameterResolver.php @@ -4,6 +4,7 @@ namespace Laminas\ServiceManager\Tool; +use ArrayAccess; use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Psr\Container\ContainerInterface; use ReflectionClass; @@ -13,6 +14,7 @@ use function array_map; use function assert; use function class_exists; +use function in_array; use function interface_exists; use function sprintf; @@ -25,7 +27,7 @@ final class ConstructorParameterResolver implements ConstructorParameterResolver public function resolveConstructorParameters( string $className, ContainerInterface $container, - array $aliases + array $aliases = [] ): array { $parameters = $this->resolveConstructorParameterServiceNamesOrFallbackTypes($className, $container, $aliases); @@ -83,7 +85,10 @@ private function resolveParameterWithConfigService( ): FallbackConstructorParameter|ServiceFromContainerConstructorParameter { if ($parameter->getName() === 'config') { $type = $parameter->getType(); - if ($type instanceof ReflectionNamedType && $type->getName() === 'array') { + if ( + $type instanceof ReflectionNamedType + && in_array($type->getName(), ['array', ArrayAccess::class], true) + ) { return new ServiceFromContainerConstructorParameter('config'); } } @@ -108,10 +113,6 @@ private function resolveParameter( $type = $parameter->getType(); $type = $type instanceof ReflectionNamedType ? $type->getName() : null; - if ($type === 'array') { - return new FallbackConstructorParameter([]); - } - if ($type === null || (! class_exists($type) && ! interface_exists($type))) { if (! $parameter->isDefaultValueAvailable()) { throw new ServiceNotFoundException(sprintf( @@ -150,7 +151,7 @@ private function resolveParameter( public function resolveConstructorParameterServiceNamesOrFallbackTypes( string $className, ContainerInterface $container, - array $aliases, + array $aliases = [], ): array { $reflectionClass = new ReflectionClass($className); diff --git a/src/Tool/ConstructorParameterResolverInterface.php b/src/Tool/ConstructorParameterResolverInterface.php index 3e9f725b..7e34af7c 100644 --- a/src/Tool/ConstructorParameterResolverInterface.php +++ b/src/Tool/ConstructorParameterResolverInterface.php @@ -18,7 +18,7 @@ interface ConstructorParameterResolverInterface public function resolveConstructorParameters( string $className, ContainerInterface $container, - array $aliases, + array $aliases = [], ): array; /** @@ -32,6 +32,6 @@ public function resolveConstructorParameters( public function resolveConstructorParameterServiceNamesOrFallbackTypes( string $className, ContainerInterface $container, - array $aliases, + array $aliases = [], ): array; } diff --git a/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php b/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php index a775e8a2..c77461e9 100644 --- a/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php +++ b/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php @@ -4,14 +4,15 @@ namespace LaminasTest\ServiceManager\AbstractFactory; -use ArrayAccess; use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Exception\ExceptionInterface; +use Laminas\ServiceManager\Exception\InvalidArgumentException; +use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; +use LaminasTest\ServiceManager\AbstractFactory\TestAsset\ClassWithConstructorAcceptingAnyArgument; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; - -use function sprintf; +use stdClass; /** * @covers \Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory @@ -23,40 +24,41 @@ final class ReflectionBasedAbstractFactoryTest extends TestCase private ReflectionBasedAbstractFactory $factory; + /** @var ConstructorParameterResolverInterface&MockObject */ + private ConstructorParameterResolverInterface $constructorParameterResolver; + protected function setUp(): void { parent::setUp(); - $this->container = $this->createMock(ContainerInterface::class); - $this->factory = new ReflectionBasedAbstractFactory(); + $this->container = $this->createMock(ContainerInterface::class); + $this->constructorParameterResolver = $this->createMock(ConstructorParameterResolverInterface::class); + $this->factory = new ReflectionBasedAbstractFactory( + [], + $this->constructorParameterResolver + ); } - public function nonClassRequestedNames(): array + /** + * @return array + */ + public function invalidRequestNames(): array { return [ - 'non-class-string' => ['non-class-string'], + 'empty-string' => [''], + 'non-existing-class' => ['non-class-string'], + 'class-with-private-constructor' => [TestAsset\ClassWithPrivateConstructor::class], ]; } /** - * @dataProvider nonClassRequestedNames + * @dataProvider invalidRequestNames */ - public function testCanCreateReturnsFalseForNonClassRequestedNames(string $requestedName): void + public function testCanCreateReturnsFalseForUnsupportedRequestNames(string $requestedName): void { self::assertFalse($this->factory->canCreate($this->container, $requestedName)); } - public function testCanCreateReturnsFalseWhenConstructorIsPrivate(): void - { - self::assertFalse( - $this->factory->canCreate( - $this->container, - TestAsset\ClassWithPrivateConstructor::class - ), - 'ReflectionBasedAbstractFactory should not be able to instantiate a class with a private constructor' - ); - } - public function testCanCreateReturnsTrueWhenClassHasNoConstructor(): void { self::assertTrue( @@ -68,207 +70,79 @@ public function testCanCreateReturnsTrueWhenClassHasNoConstructor(): void ); } - public function testFactoryInstantiatesClassDirectlyIfItHasNoConstructor(): void - { - $instance = $this->factory->__invoke($this->container, TestAsset\ClassWithNoConstructor::class); - - self::assertInstanceOf(TestAsset\ClassWithNoConstructor::class, $instance); - } - - public function testFactoryInstantiatesClassDirectlyIfConstructorHasNoArguments(): void - { - $instance = $this->factory->__invoke($this->container, TestAsset\ClassWithEmptyConstructor::class); - - self::assertInstanceOf(TestAsset\ClassWithEmptyConstructor::class, $instance); - } - - public function testFactoryRaisesExceptionWhenUnableToResolveATypeHintedService(): void + /** + * @return array + */ + public function classNamesWithoutConstructorArguments(): array { - $this->container - ->expects(self::exactly(2)) - ->method('has') - ->withConsecutive( - ['config'], - [TestAsset\SampleInterface::class], - ) - ->willReturn(false, false); - - $this->expectException(ServiceNotFoundException::class); - $this->expectExceptionMessage(sprintf( - 'Unable to create service "%s"; unable to resolve parameter "sample" using type hint "%s"', - TestAsset\ClassWithTypeHintedConstructorParameter::class, - TestAsset\SampleInterface::class - )); - - $this->factory->__invoke($this->container, TestAsset\ClassWithTypeHintedConstructorParameter::class); + return [ + 'no-constructor' => [ + TestAsset\ClassWithNoConstructor::class, + ], + 'no-constructor-arguments' => [ + TestAsset\ClassWithEmptyConstructor::class, + ], + ]; } - public function testFactoryRaisesExceptionForScalarParameters(): void + /** + * @param class-string $className + * @dataProvider classNamesWithoutConstructorArguments + */ + public function testFactoryInstantiatesClassWithoutConstructorArguments(string $className): void { - $this->expectException(ServiceNotFoundException::class); - $this->expectExceptionMessage(sprintf( - 'Unable to create service "%s"; unable to resolve parameter "foo" to a class, interface, or array type', - TestAsset\ClassWithScalarParameters::class - )); + $instance = $this->factory->__invoke($this->container, $className); - $this->factory->__invoke($this->container, TestAsset\ClassWithScalarParameters::class); + self::assertInstanceOf($className, $instance); } - public function testFactoryInjectsConfigServiceForConfigArgumentsTypeHintedAsArray(): void + public function testWillThrowInvalidArgumentExceptionForInExistentClassName(): void { - $config = ['foo' => 'bar']; - - $this->container - ->expects(self::once()) - ->method('has') - ->with('config') - ->willReturn(true); - - $this->container - ->expects(self::once()) - ->method('get') - ->with('config') - ->willReturn($config); - - $instance = $this->factory->__invoke($this->container, TestAsset\ClassAcceptingConfigToConstructor::class); - - self::assertInstanceOf(TestAsset\ClassAcceptingConfigToConstructor::class, $instance); - self::assertSame($config, $instance->config); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('can only be used with class names.'); + $this->factory->__invoke($this->container, 'serviceName'); } - public function testFactoryCanInjectKnownTypeHintedServices(): void + public function testFactoryPassesContainerExceptions(): void { - $this->container - ->expects(self::exactly(2)) - ->method('has') - ->withConsecutive( - ['config'], - [TestAsset\SampleInterface::class], - ) - ->willReturn(false, true); + $this->expectException(ExceptionInterface::class); + $this->constructorParameterResolver + ->method('resolveConstructorParameters') + ->with(stdClass::class) + ->willThrowException($this->createMock(ExceptionInterface::class)); - $sample = $this->createMock(TestAsset\SampleInterface::class); - - $this->container - ->expects(self::once()) - ->method('get') - ->with(TestAsset\SampleInterface::class) - ->willReturn($sample); - - $instance = $this->factory->__invoke( - $this->container, - TestAsset\ClassWithTypeHintedConstructorParameter::class, - ); - - self::assertInstanceOf(TestAsset\ClassWithTypeHintedConstructorParameter::class, $instance); - self::assertSame($sample, $instance->sample); + $this->factory->__invoke($this->container, stdClass::class); } - public function testFactoryResolvesTypeHintsForServicesToWellKnownServiceNames(): void + public function testFactoryPassesAliasesToArgumentResolver(): void { - $this->container - ->expects(self::exactly(2)) - ->method('has') - ->withConsecutive( - ['config'], - ['ValidatorManager'], - ) - ->willReturn(false, true); - - $validators = $this->createMock(TestAsset\ValidatorPluginManager::class); + $factory = new ReflectionBasedAbstractFactory([ + 'Foo' => 'Bar', + ], $this->constructorParameterResolver); - $this->container + $this->constructorParameterResolver ->expects(self::once()) - ->method('get') - ->with('ValidatorManager') - ->willReturn($validators); - - $factory = new ReflectionBasedAbstractFactory([TestAsset\ValidatorPluginManager::class => 'ValidatorManager']); - $instance = $factory( - $this->container, - TestAsset\ClassAcceptingWellKnownServicesAsConstructorParameters::class - ); + ->method('resolveConstructorParameters') + ->with(stdClass::class, $this->container, ['Foo' => 'Bar']); - self::assertInstanceOf( - TestAsset\ClassAcceptingWellKnownServicesAsConstructorParameters::class, - $instance - ); - self::assertSame($validators, $instance->validators); + $factory->__invoke($this->container, stdClass::class); } - public function testFactoryCanSupplyAMixOfParameterTypes(): void + public function testPassesConstructorArgumentsInTheSameOrderAsReturnedFromResolver(): void { - $this->container - ->expects(self::exactly(3)) - ->method('has') - ->withConsecutive( - ['config'], - [TestAsset\SampleInterface::class], - ['ValidatorManager'], - ) - ->willReturn(true, true, true); - - $config = ['foo' => 'bar']; - $sample = $this->createMock(TestAsset\SampleInterface::class); - $validators = $this->createMock(TestAsset\ValidatorPluginManager::class); - - $this->container - ->expects(self::exactly(3)) - ->method('get') - ->withConsecutive( - ['config'], - [TestAsset\SampleInterface::class], - ['ValidatorManager'], - ) - ->willReturn($config, $sample, $validators); + $resolvedParameters = ['foo', true, 1, 0.0, static fn (): bool => true]; - $factory = new ReflectionBasedAbstractFactory([TestAsset\ValidatorPluginManager::class => 'ValidatorManager']); - $instance = $factory->__invoke($this->container, TestAsset\ClassWithMixedConstructorParameters::class); - - self::assertInstanceOf(TestAsset\ClassWithMixedConstructorParameters::class, $instance); - self::assertSame($config, $instance->config); - self::assertSame([], $instance->options); - self::assertSame($sample, $instance->sample); - self::assertSame($validators, $instance->validators); - } - - public function testFactoryWillUseDefaultValueWhenPresentForScalarArgument(): void - { - $this->container + $this->constructorParameterResolver ->expects(self::once()) - ->method('has') - ->with('config') - ->willReturn(false); - - $instance = $this->factory->__invoke( - $this->container, - TestAsset\ClassWithScalarDependencyDefiningDefaultValue::class - ); - - self::assertInstanceOf(TestAsset\ClassWithScalarDependencyDefiningDefaultValue::class, $instance); - self::assertSame('bar', $instance->foo); - } - - /** - * @see https://github.com/zendframework/zend-servicemanager/issues/239 - */ - public function testFactoryWillUseDefaultValueForTypeHintedArgument(): void - { - $this->container - ->expects(self::exactly(2)) - ->method('has') - ->withConsecutive( - ['config'], - [ArrayAccess::class], - ) - ->willReturn(false, false); - - $instance = $this->factory->__invoke( - $this->container, - TestAsset\ClassWithTypehintedDefaultValue::class - ); - - self::assertInstanceOf(TestAsset\ClassWithTypehintedDefaultValue::class, $instance); - self::assertNull($instance->value); + ->method('resolveConstructorParameters') + ->willReturn($resolvedParameters); + + $factory = new ReflectionBasedAbstractFactory([], $this->constructorParameterResolver); + $instance = $factory->__invoke($this->container, ClassWithConstructorAcceptingAnyArgument::class); + self::assertInstanceOf(ClassWithConstructorAcceptingAnyArgument::class, $instance); + foreach ($resolvedParameters as $index => $parameter) { + self::assertArrayHasKey($index, $instance->arguments); + self::assertSame($parameter, $instance->arguments[$index]); + } } } diff --git a/test/AbstractFactory/TestAsset/ClassWithConstructorAcceptingAnyArgument.php b/test/AbstractFactory/TestAsset/ClassWithConstructorAcceptingAnyArgument.php new file mode 100644 index 00000000..a21e6dce --- /dev/null +++ b/test/AbstractFactory/TestAsset/ClassWithConstructorAcceptingAnyArgument.php @@ -0,0 +1,16 @@ +arguments = $arguments; + } +} diff --git a/test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultValue.php b/test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultNullValue.php similarity index 84% rename from test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultValue.php rename to test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultNullValue.php index 8ce71187..e6bf55fd 100644 --- a/test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultValue.php +++ b/test/AbstractFactory/TestAsset/ClassWithTypehintedDefaultNullValue.php @@ -6,7 +6,7 @@ use ArrayAccess; -final class ClassWithTypehintedDefaultValue +final class ClassWithTypehintedDefaultNullValue { public ?ArrayAccess $value; diff --git a/test/TestAsset/ClassWithConstructorWithOnlyOptionalArguments.php b/test/TestAsset/ClassWithConstructorWithOnlyOptionalArguments.php new file mode 100644 index 00000000..ae056d17 --- /dev/null +++ b/test/TestAsset/ClassWithConstructorWithOnlyOptionalArguments.php @@ -0,0 +1,18 @@ +resolver = new ConstructorParameterResolver(); + $this->container = $this->createMock(ContainerInterface::class); + } + + public function testCanHandleClassNameWithoutConstructor(): void + { + $container = $this->createMock(ContainerInterface::class); + $parameters = $this->resolver->resolveConstructorParameterServiceNamesOrFallbackTypes( + ClassWithNoConstructor::class, + $container + ); + self::assertSame([], $parameters); + } + + public function testCanHandleClassNameWithOptionalConstructorDependencies(): void + { + $container = $this->createMock(ContainerInterface::class); + $parameters = $this->resolver->resolveConstructorParameterServiceNamesOrFallbackTypes( + ClassWithConstructorWithOnlyOptionalArguments::class, + $container + ); + $expectedResolvedParameters = [ + [], + '', + true, + 1, + 0.0, + null, + ]; + + self::assertSameSize($expectedResolvedParameters, $parameters); + foreach ($parameters as $index => $parameter) { + self::assertInstanceOf(FallbackConstructorParameter::class, $parameter); + $expectedParameter = $expectedResolvedParameters[$index] ?? null; + self::assertSame($expectedParameter, $parameter->argumentValue); + } + } + + public function testWillDetectRequiredConstructorArguments(): void + { + $container = $this->createMock(ContainerInterface::class); + $container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap([ + ['config', false], + [FactoryInterface::class, true], + ]); + + $parameters = $this->resolver->resolveConstructorParameterServiceNamesOrFallbackTypes( + ClassDependingOnAnInterface::class, + $container + ); + self::assertCount(1, $parameters); + self::assertInstanceOf(ServiceFromContainerConstructorParameter::class, $parameters[0]); + $parameter = $parameters[0]; + self::assertSame(FactoryInterface::class, $parameter->serviceName); + } + + public function testRaisesExceptionWhenUnableToResolveATypeHintedService(): void + { + $this->container + ->expects(self::exactly(2)) + ->method('has') + ->withConsecutive( + ['config'], + [SampleInterface::class], + ) + ->willReturn(false, false); + + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage(sprintf( + 'Unable to create service "%s"; unable to resolve parameter "sample" using type hint "%s"', + ClassWithTypeHintedConstructorParameter::class, + SampleInterface::class + )); + + $this->resolver->resolveConstructorParameters(ClassWithTypeHintedConstructorParameter::class, $this->container); + } + + public function testRaisesExceptionForScalarParameters(): void + { + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage(sprintf( + 'Unable to create service "%s"; unable to resolve parameter "foo" to a class, interface, or array type', + ClassWithScalarParameters::class + )); + + $this->resolver->resolveConstructorParameters(ClassWithScalarParameters::class, $this->container); + } + + public function testResolvesConfigServiceForConfigArgumentsTypeHintedAsArray(): void + { + $config = ['foo' => 'bar']; + + $this->container + ->expects(self::once()) + ->method('has') + ->with('config') + ->willReturn(true); + + $this->container + ->expects(self::once()) + ->method('get') + ->with('config') + ->willReturn($config); + + $parameters = $this->resolver->resolveConstructorParameters( + ClassAcceptingConfigToConstructor::class, + $this->container + ); + self::assertCount(1, $parameters); + self::assertSame($config, $parameters[0]); + } + + public function testFactoryCanInjectKnownTypeHintedServices(): void + { + $this->container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap([ + ['config', false], + [SampleInterface::class, true], + ]); + + $sample = $this->createMock(SampleInterface::class); + + $this->container + ->expects(self::once()) + ->method('get') + ->with(SampleInterface::class) + ->willReturn($sample); + + $parameters = $this->resolver->resolveConstructorParameters( + ClassWithTypeHintedConstructorParameter::class, + $this->container, + ); + + self::assertCount(1, $parameters); + self::assertSame($sample, $parameters[0]); + } + + public function testResolvesTypeHintsForServicesToWellKnownServiceNames(): void + { + $this->container + ->expects(self::exactly(2)) + ->method('has') + ->willReturnMap([ + ['config', false], + ['ValidatorManager', true], + ]); + + $validators = $this->createMock(ValidatorPluginManager::class); + + $this->container + ->expects(self::once()) + ->method('get') + ->with('ValidatorManager') + ->willReturn($validators); + + $parameters = $this->resolver->resolveConstructorParameters( + ClassAcceptingWellKnownServicesAsConstructorParameters::class, + $this->container, + [ValidatorPluginManager::class => 'ValidatorManager'], + ); + + self::assertCount(1, $parameters); + self::assertSame($validators, $parameters[0]); + } + + /** + * @depends testWillResolveConstructorArgumentsAccordingToTheirPosition + */ + public function testResolvesAMixOfParameterTypes(): void + { + $this->container + ->expects(self::exactly(3)) + ->method('has') + ->willReturnMap([ + ['config', true], + [SampleInterface::class, true], + ['ValidatorManager', true], + ]); + + $config = ['foo' => 'bar']; + $sample = $this->createMock(SampleInterface::class); + $validators = $this->createMock(ValidatorPluginManager::class); + + $this->container + ->expects(self::exactly(3)) + ->method('get') + ->willReturnMap([ + ['config', $config], + [SampleInterface::class, $sample], + ['ValidatorManager', $validators], + ]); + + $parameters = $this->resolver->resolveConstructorParameters( + ClassWithMixedConstructorParameters::class, + $this->container, + [ValidatorPluginManager::class => 'ValidatorManager'] + ); + + self::assertCount(4, $parameters); + self::assertSame($config, $parameters[0]); + self::assertSame($sample, $parameters[1]); + self::assertSame($validators, $parameters[2]); + self::assertNull($parameters[3], 'Optional parameters should resolve to their default value.'); + } + + public function testResolvesDefaultValuesWhenPresentForScalarArgument(): void + { + $parameters = $this->resolver->resolveConstructorParameters( + ClassWithScalarDependencyDefiningDefaultValue::class, + $this->container, + ); + + self::assertCount(1, $parameters); + self::assertSame('bar', $parameters[0]); + } + + /** + * @see https://github.com/zendframework/zend-servicemanager/issues/239 + */ + public function testWillResolveToDefaultValueForTypeHintedArgumentWhichDoesNotExistInContainer(): void + { + $parameters = $this->resolver->resolveConstructorParameters( + ClassWithTypehintedDefaultNullValue::class, + $this->container, + ); + + self::assertCount(1, $parameters); + self::assertNull($parameters[0]); + } + + public function testWillResolveConstructorArgumentsAccordingToTheirPosition(): void + { + $this->container + ->method('has') + ->willReturnMap([ + ['config', true], + [SampleInterface::class, true], + [ValidatorPluginManager::class, true], + ]); + + $sample = $this->createMock(SampleInterface::class); + $validators = $this->createMock(ValidatorPluginManager::class); + + $this->container + ->method('get') + ->willReturnMap([ + ['config', ['foo' => 'bar']], + [SampleInterface::class, $sample], + [ValidatorPluginManager::class, $validators], + ]); + + $parameters = $this->resolver->resolveConstructorParameters( + ClassWithMixedConstructorParameters::class, + $this->container + ); + + self::assertCount(4, $parameters); + self::assertSame(['foo' => 'bar'], $parameters[0]); + self::assertSame($sample, $parameters[1]); + self::assertSame($validators, $parameters[2]); + self::assertNull($parameters[3], 'Optional parameters should resolve to their default value.'); + } +} From dc82a21a9306f107e230517afdc173b62efd6c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 02:56:35 +0200 Subject: [PATCH 24/32] qa: move more complex tools into dedicated namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/AbstractFactory/ReflectionBasedAbstractFactory.php | 4 ++-- src/Command/AheadOfTimeFactoryCreatorCommand.php | 2 +- src/Command/AheadOfTimeFactoryCreatorCommandFactory.php | 2 +- src/ConfigProvider.php | 8 ++++---- .../AheadOfTimeCompiledFactory.php | 2 +- .../AheadOfTimeFactoryCompiler.php | 3 ++- .../AheadOfTimeFactoryCompilerFactory.php | 3 ++- .../AheadOfTimeFactoryCompilerInterface.php | 2 +- .../ConstructorParameterResolver.php | 2 +- .../ConstructorParameterResolverInterface.php | 2 +- .../FallbackConstructorParameter.php | 2 +- .../ServiceFromContainerConstructorParameter.php | 2 +- src/Tool/FactoryCreator.php | 3 +++ src/Tool/FactoryCreatorFactory.php | 1 + .../ReflectionBasedAbstractFactoryTest.php | 2 +- test/Command/AheadOfTimeFactoryCreatorCommandTest.php | 4 ++-- .../ConstructorParameterResolverTest.php | 8 ++++---- 17 files changed, 29 insertions(+), 23 deletions(-) rename src/Tool/{ => AheadOfTimeFactoryCompiler}/AheadOfTimeCompiledFactory.php (87%) rename src/Tool/{ => AheadOfTimeFactoryCompiler}/AheadOfTimeFactoryCompiler.php (96%) rename src/Tool/{ => AheadOfTimeFactoryCompiler}/AheadOfTimeFactoryCompilerFactory.php (72%) rename src/Tool/{ => AheadOfTimeFactoryCompiler}/AheadOfTimeFactoryCompilerInterface.php (75%) rename src/Tool/{ => ConstructorParameterResolver}/ConstructorParameterResolver.php (98%) rename src/Tool/{ => ConstructorParameterResolver}/ConstructorParameterResolverInterface.php (94%) rename src/Tool/{ => ConstructorParameterResolver}/FallbackConstructorParameter.php (70%) rename src/Tool/{ => ConstructorParameterResolver}/ServiceFromContainerConstructorParameter.php (77%) rename test/Tool/{ => ConstructorParameterResolver}/ConstructorParameterResolverTest.php (96%) diff --git a/src/AbstractFactory/ReflectionBasedAbstractFactory.php b/src/AbstractFactory/ReflectionBasedAbstractFactory.php index 27f078e5..dbda3046 100644 --- a/src/AbstractFactory/ReflectionBasedAbstractFactory.php +++ b/src/AbstractFactory/ReflectionBasedAbstractFactory.php @@ -6,8 +6,8 @@ use Laminas\ServiceManager\Exception\InvalidArgumentException; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; -use Laminas\ServiceManager\Tool\ConstructorParameterResolver; -use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolverInterface; use Psr\Container\ContainerInterface; use ReflectionClass; diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index 93397527..65b470fe 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -9,7 +9,7 @@ use Laminas\ServiceManager\ConfigProvider; use Laminas\ServiceManager\Exception\RuntimeException; use Laminas\ServiceManager\Factory\FactoryInterface; -use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerInterface; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeFactoryCompilerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; diff --git a/src/Command/AheadOfTimeFactoryCreatorCommandFactory.php b/src/Command/AheadOfTimeFactoryCreatorCommandFactory.php index 7958534c..84fa91b4 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommandFactory.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommandFactory.php @@ -5,7 +5,7 @@ namespace Laminas\ServiceManager\Command; use Laminas\ServiceManager\ConfigProvider; -use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerInterface; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeFactoryCompilerInterface; use Psr\Container\ContainerInterface; use function is_array; diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 4816f156..abc79e56 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -9,12 +9,12 @@ use Laminas\ServiceManager\Command\ConfigDumperCommand; use Laminas\ServiceManager\Command\FactoryCreatorCommand; use Laminas\ServiceManager\Factory\InvokableFactory; -use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerFactory; -use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerInterface; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeFactoryCompilerFactory; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeFactoryCompilerInterface; use Laminas\ServiceManager\Tool\ConfigDumperFactory; use Laminas\ServiceManager\Tool\ConfigDumperInterface; -use Laminas\ServiceManager\Tool\ConstructorParameterResolver; -use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolverInterface; use Laminas\ServiceManager\Tool\FactoryCreatorFactory; use Laminas\ServiceManager\Tool\FactoryCreatorInterface; use Symfony\Component\Console\Command\Command; diff --git a/src/Tool/AheadOfTimeCompiledFactory.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php similarity index 87% rename from src/Tool/AheadOfTimeCompiledFactory.php rename to src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php index df32e541..f7394b3b 100644 --- a/src/Tool/AheadOfTimeCompiledFactory.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler; final class AheadOfTimeCompiledFactory { diff --git a/src/Tool/AheadOfTimeFactoryCompiler.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php similarity index 96% rename from src/Tool/AheadOfTimeFactoryCompiler.php rename to src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php index 2fa04ae9..2e2c49ac 100644 --- a/src/Tool/AheadOfTimeFactoryCompiler.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php @@ -2,10 +2,11 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler; use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; use Laminas\ServiceManager\Exception\InvalidArgumentException; +use Laminas\ServiceManager\Tool\FactoryCreatorInterface; use function array_filter; use function array_key_exists; diff --git a/src/Tool/AheadOfTimeFactoryCompilerFactory.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php similarity index 72% rename from src/Tool/AheadOfTimeFactoryCompilerFactory.php rename to src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php index 8a13ea25..2e997707 100644 --- a/src/Tool/AheadOfTimeFactoryCompilerFactory.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler; +use Laminas\ServiceManager\Tool\FactoryCreatorInterface; use Psr\Container\ContainerInterface; final class AheadOfTimeFactoryCompilerFactory diff --git a/src/Tool/AheadOfTimeFactoryCompilerInterface.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php similarity index 75% rename from src/Tool/AheadOfTimeFactoryCompilerInterface.php rename to src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php index 16d807bc..c954a22f 100644 --- a/src/Tool/AheadOfTimeFactoryCompilerInterface.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler; interface AheadOfTimeFactoryCompilerInterface { diff --git a/src/Tool/ConstructorParameterResolver.php b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php similarity index 98% rename from src/Tool/ConstructorParameterResolver.php rename to src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php index 3ce5500f..6e1830e0 100644 --- a/src/Tool/ConstructorParameterResolver.php +++ b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\ConstructorParameterResolver; use ArrayAccess; use Laminas\ServiceManager\Exception\ServiceNotFoundException; diff --git a/src/Tool/ConstructorParameterResolverInterface.php b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolverInterface.php similarity index 94% rename from src/Tool/ConstructorParameterResolverInterface.php rename to src/Tool/ConstructorParameterResolver/ConstructorParameterResolverInterface.php index 7e34af7c..45be5474 100644 --- a/src/Tool/ConstructorParameterResolverInterface.php +++ b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolverInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\ConstructorParameterResolver; use Psr\Container\ContainerInterface; diff --git a/src/Tool/FallbackConstructorParameter.php b/src/Tool/ConstructorParameterResolver/FallbackConstructorParameter.php similarity index 70% rename from src/Tool/FallbackConstructorParameter.php rename to src/Tool/ConstructorParameterResolver/FallbackConstructorParameter.php index 5d0ced09..c36cc438 100644 --- a/src/Tool/FallbackConstructorParameter.php +++ b/src/Tool/ConstructorParameterResolver/FallbackConstructorParameter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\ConstructorParameterResolver; final class FallbackConstructorParameter { diff --git a/src/Tool/ServiceFromContainerConstructorParameter.php b/src/Tool/ConstructorParameterResolver/ServiceFromContainerConstructorParameter.php similarity index 77% rename from src/Tool/ServiceFromContainerConstructorParameter.php rename to src/Tool/ConstructorParameterResolver/ServiceFromContainerConstructorParameter.php index 37cf5951..1da48d3c 100644 --- a/src/Tool/ServiceFromContainerConstructorParameter.php +++ b/src/Tool/ConstructorParameterResolver/ServiceFromContainerConstructorParameter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\ServiceManager\Tool; +namespace Laminas\ServiceManager\Tool\ConstructorParameterResolver; final class ServiceFromContainerConstructorParameter { diff --git a/src/Tool/FactoryCreator.php b/src/Tool/FactoryCreator.php index a2ea814f..4832f0f0 100644 --- a/src/Tool/FactoryCreator.php +++ b/src/Tool/FactoryCreator.php @@ -7,6 +7,9 @@ use Brick\VarExporter\VarExporter; use Laminas\ServiceManager\Factory\FactoryInterface; use Laminas\ServiceManager\ServiceManager; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolverInterface; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ServiceFromContainerConstructorParameter; use Psr\Container\ContainerInterface; use function array_map; diff --git a/src/Tool/FactoryCreatorFactory.php b/src/Tool/FactoryCreatorFactory.php index 02880078..cb487ca3 100644 --- a/src/Tool/FactoryCreatorFactory.php +++ b/src/Tool/FactoryCreatorFactory.php @@ -4,6 +4,7 @@ namespace Laminas\ServiceManager\Tool; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolverInterface; use Psr\Container\ContainerInterface; /** diff --git a/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php b/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php index c77461e9..99ff2e09 100644 --- a/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php +++ b/test/AbstractFactory/ReflectionBasedAbstractFactoryTest.php @@ -7,7 +7,7 @@ use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; use Laminas\ServiceManager\Exception\ExceptionInterface; use Laminas\ServiceManager\Exception\InvalidArgumentException; -use Laminas\ServiceManager\Tool\ConstructorParameterResolverInterface; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolverInterface; use LaminasTest\ServiceManager\AbstractFactory\TestAsset\ClassWithConstructorAcceptingAnyArgument; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php index 6382f89e..bd9079b1 100644 --- a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -6,8 +6,8 @@ use Laminas\ServiceManager\Command\AheadOfTimeFactoryCreatorCommand; use Laminas\ServiceManager\ConfigProvider; -use Laminas\ServiceManager\Tool\AheadOfTimeCompiledFactory; -use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompilerInterface; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeCompiledFactory; +use Laminas\ServiceManager\Tool\AheadOfTimeFactoryCompiler\AheadOfTimeFactoryCompilerInterface; use LaminasTest\ServiceManager\TestAsset\SimpleDependencyObject; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; diff --git a/test/Tool/ConstructorParameterResolverTest.php b/test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php similarity index 96% rename from test/Tool/ConstructorParameterResolverTest.php rename to test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php index 7341b1eb..37a72c0f 100644 --- a/test/Tool/ConstructorParameterResolverTest.php +++ b/test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace LaminasTest\ServiceManager\Tool; +namespace LaminasTest\ServiceManager\Tool\ConstructorParameterResolver; use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Laminas\ServiceManager\Factory\FactoryInterface; -use Laminas\ServiceManager\Tool\ConstructorParameterResolver; -use Laminas\ServiceManager\Tool\FallbackConstructorParameter; -use Laminas\ServiceManager\Tool\ServiceFromContainerConstructorParameter; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\FallbackConstructorParameter; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ServiceFromContainerConstructorParameter; use LaminasTest\ServiceManager\AbstractFactory\TestAsset\ClassAcceptingConfigToConstructor; use LaminasTest\ServiceManager\AbstractFactory\TestAsset\ClassAcceptingWellKnownServicesAsConstructorParameters; use LaminasTest\ServiceManager\AbstractFactory\TestAsset\ClassWithMixedConstructorParameters; From e7827c4be0c8eab61abd790c581fde12245d6a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:35:15 +0200 Subject: [PATCH 25/32] qa: add failing test for already existing factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../AheadOfTimeFactoryCreatorCommandTest.php | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php index bd9079b1..de1fc706 100644 --- a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -116,7 +116,7 @@ public function testWillNotCreateConfigurationFileWhenNoFactoriesDetected(): voi ->method('compile') ->willReturn([]); - $command->run($this->input, $this->output); + self::assertSame(0, $command->run($this->input, $this->output)); self::assertCount(0, $this->factoryTargetPath->getChildren()); } @@ -160,7 +160,7 @@ public function testWillCreateExpectedGeneratedFactoriesConfig(): void ->method('writeln') ->with('Successfully created 1 factories.'); - $command->run($this->input, $this->output); + self::assertSame(0, $command->run($this->input, $this->output)); self::assertCount(2, $this->factoryTargetPath->getChildren()); self::assertTrue($this->factoryTargetPath->hasChild('foobar')); @@ -245,6 +245,51 @@ public function testWillVerifyLocalConfigFilenameIsWritable(): void $localConfigPath, )); - $command->run($this->input, $this->output); + self::assertSame(1, $command->run($this->input, $this->output)); + } + + /** + * @requires testWillVerifyLocalConfigFilenameIsWritable + */ + public function testWillDetectAlreadyExistingFactories(): void + { + $directory = $this->factoryTargetPath->url(); + + $command = new AheadOfTimeFactoryCreatorCommand( + [], + $directory, + $this->factoryCompiler, + ); + + $localConfigFilename = 'yada-yada.local.php'; + + $this->input + ->method('getArgument') + ->with('localConfigFilename') + ->willReturn(sprintf('%s/%s', $directory, $localConfigFilename)); + + $generatedFactoryAssetPath = __DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'; + $generatedFactory = file_get_contents($generatedFactoryAssetPath); + assert($generatedFactory !== ''); + + $this->factoryCompiler + ->expects(self::once()) + ->method('compile') + ->willReturn([ + new AheadOfTimeCompiledFactory( + SimpleDependencyObject::class, + 'foobar', + $generatedFactory, + ), + ]); + + $this->assertErrorRaised( + 'There is already an existing factory class registered for' + .' "LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObject": LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObjectFactory' + ); + + require $generatedFactoryAssetPath; + + self::assertSame(1, $command->run($this->input, $this->output)); } } From b6946b1fd78f060a666eb766dfb900ac868a2665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:36:20 +0200 Subject: [PATCH 26/32] feature: detect already existing factories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 <2189546+boesing@users.noreply.github.com> --- src/Command/AheadOfTimeFactoryCreatorCommand.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index 65b470fe..168aefe5 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface; use function assert; +use function class_exists; use function count; use function dirname; use function file_put_contents; @@ -105,14 +106,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int preg_replace('/\W/', '', $factory->containerConfigurationKey) ); + /** @var class-string $factoryClassName */ + $factoryClassName = sprintf('%sFactory', $factory->fullyQualifiedClassName); + if (class_exists($factoryClassName)) { + $output->writeln(sprintf( + 'There is already an existing factory class registered for "%s": %s', + $factory->fullyQualifiedClassName, + $factoryClassName, + )); + + return self::FAILURE; + } + if (! is_dir($targetDirectory)) { if (! mkdir($targetDirectory, recursive: true) && ! is_dir($targetDirectory)) { throw new RuntimeException(sprintf('Unable to create directory "%s".', $targetDirectory)); } } - /** @var class-string $factoryClassName */ - $factoryClassName = sprintf('%sFactory', $factory->fullyQualifiedClassName); $factoryFileName = sprintf( '%s/%s.php', $targetDirectory, From b9675b2077c32f623a61fad7aa159fdd583006a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:36:48 +0200 Subject: [PATCH 27/32] qa: do not allow factory compilation for enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../AheadOfTimeFactoryCompiler.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php index 2e2c49ac..a7efc1e5 100644 --- a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php @@ -11,11 +11,13 @@ use function array_filter; use function array_key_exists; use function class_exists; +use function enum_exists; use function is_array; use function is_string; use function sprintf; use const ARRAY_FILTER_USE_BOTH; +use const PHP_VERSION_ID; final class AheadOfTimeFactoryCompiler implements AheadOfTimeFactoryCompilerInterface { @@ -73,7 +75,7 @@ private function extractServicesRegisteredByReflectionBasedFactory(array $config } foreach ($servicesUsingReflectionBasedFactory as $service => $factory) { - if (! class_exists($service)) { + if (!$this->canServiceBeUsedWithReflectionBasedFactory($service)) { throw new InvalidArgumentException(sprintf( 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist or does' . ' not refer to an actual class.', @@ -101,4 +103,23 @@ private function extractServicesRegisteredByReflectionBasedFactory(array $config return $services; } + + /** + * Starting with PHP 8.1, `class_exists` resolves to `true` for enums. + * @link https://3v4l.org/FY7eg + * + * @psalm-assert-if-true class-string $service + */ + private function canServiceBeUsedWithReflectionBasedFactory(string $service): bool + { + if (! class_exists($service)) { + return false; + } + + if (PHP_VERSION_ID < 80100) { + return true; + } + + return !enum_exists($service); + } } From 151d34d4319ab409715b1f0811f6f1c2fd123cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:37:00 +0200 Subject: [PATCH 28/32] qa: add tests for the `AheadOfTimeFactoryCompiler` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../AheadOfTimeFactoryCreatorCommand.php | 2 +- .../AheadOfTimeFactoryCompiler.php | 5 +- .../AheadOfTimeFactoryCompilerTest.php | 151 ++++++++++++++++++ .../TestAsset/WhateverEnum.php | 9 ++ .../TestAsset/WhateverTrait.php | 8 + 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php create mode 100644 test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php create mode 100644 test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverTrait.php diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php index 168aefe5..c054c309 100644 --- a/src/Command/AheadOfTimeFactoryCreatorCommand.php +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -124,7 +124,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $factoryFileName = sprintf( + $factoryFileName = sprintf( '%s/%s.php', $targetDirectory, str_replace('\\', '_', $factoryClassName) diff --git a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php index a7efc1e5..fa0aed2a 100644 --- a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompiler.php @@ -75,7 +75,7 @@ private function extractServicesRegisteredByReflectionBasedFactory(array $config } foreach ($servicesUsingReflectionBasedFactory as $service => $factory) { - if (!$this->canServiceBeUsedWithReflectionBasedFactory($service)) { + if (! $this->canServiceBeUsedWithReflectionBasedFactory($service)) { throw new InvalidArgumentException(sprintf( 'Configured service "%s" using the `ReflectionBasedAbstractFactory` does not exist or does' . ' not refer to an actual class.', @@ -106,6 +106,7 @@ private function extractServicesRegisteredByReflectionBasedFactory(array $config /** * Starting with PHP 8.1, `class_exists` resolves to `true` for enums. + * * @link https://3v4l.org/FY7eg * * @psalm-assert-if-true class-string $service @@ -120,6 +121,6 @@ private function canServiceBeUsedWithReflectionBasedFactory(string $service): bo return true; } - return !enum_exists($service); + return ! enum_exists($service); } } diff --git a/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php new file mode 100644 index 00000000..85bfcbd8 --- /dev/null +++ b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php @@ -0,0 +1,151 @@ +factoryCreator = $this->createMock(FactoryCreatorInterface::class); + $this->compiler = new AheadOfTimeFactoryCompiler( + $this->factoryCreator, + ); + } + + public function testCanHandleConfigWithoutServicesRegisteredWithReflectionBasedAbstractFactory(): void + { + $this->factoryCreator + ->expects(self::never()) + ->method(self::anything()); + + self::assertSame([], $this->compiler->compile([])); + } + + public function testCanHandleLaminasMvcServiceManagerConfiguration(): void + { + $config = [ + 'service_manager' => [ + 'factories' => [ + stdClass::class => ReflectionBasedAbstractFactory::class, + ], + ], + ]; + + $this->factoryCreator + ->expects(self::once()) + ->method('createFactory') + ->with(stdClass::class) + ->willReturn('created factory'); + + $factories = $this->compiler->compile($config); + self::assertCount(1, $factories); + $factory = $factories[0]; + self::assertSame('service_manager', $factory->containerConfigurationKey); + self::assertSame(stdClass::class, $factory->fullyQualifiedClassName); + self::assertSame('created factory', $factory->generatedFactory); + } + + /** + * @return array + */ + public function nonClassReferencingServiceNames(): array + { + return [ + 'nonexistent-service-name' => [ + 'foobar', + ], + 'interface' => [ + FactoryInterface::class, + ], + 'trait' => [ + WhateverTrait::class, + ], + ]; + } + + /** + * @return array + */ + public function nonClassReferencingServiceNamesPhp81Upwards(): array + { + if (PHP_VERSION_ID < 80100) { + return []; + } + + return [ + 'enum' => [ + WhateverEnum::class, + ], + ]; + } + + /** + * @dataProvider nonClassReferencingServiceNames + * @dataProvider nonClassReferencingServiceNamesPhp81Upwards + */ + public function testWillRaiseExceptionWhenFactoryIsUsedWithNonClassReferencingService(string $serviceName): void + { + $config = [ + 'dependencies' => [ + 'factories' => [ + $serviceName => ReflectionBasedAbstractFactory::class, + ], + ], + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('does not exist or does not refer to an actual class'); + + $this->factoryCreator + ->expects(self::never()) + ->method(self::anything()); + + $this->compiler->compile($config); + } + + public function testWillDetectSameServiceProvidedByMultipleServiceOrPluginManagers(): void + { + $config = [ + 'foo' => [ + 'factories' => [ + stdClass::class => ReflectionBasedAbstractFactory::class, + ], + ], + 'bar' => [ + 'factories' => [ + stdClass::class => ReflectionBasedAbstractFactory::class, + ], + ], + ]; + + $this->factoryCreator + ->expects(self::never()) + ->method(self::anything()); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('is registered in (at least) two service-/plugin-managers: foo, bar'); + + $this->compiler->compile($config); + } +} diff --git a/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php b/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php new file mode 100644 index 00000000..548b2ace --- /dev/null +++ b/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php @@ -0,0 +1,9 @@ + Date: Mon, 10 Apr 2023 00:07:47 +0200 Subject: [PATCH 29/32] qa: add some more unit tests for `AheadOfTimeFactoryCompiler` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../AheadOfTimeFactoryCreatorCommandTest.php | 4 +- .../AheadOfTimeFactoryCompilerTest.php | 119 ++++++++++++++++-- .../TestAsset/WhateverEnum.php | 1 + .../TestAsset/WhateverTrait.php | 1 + 4 files changed, 115 insertions(+), 10 deletions(-) diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php index de1fc706..2afed3f8 100644 --- a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -269,7 +269,7 @@ public function testWillDetectAlreadyExistingFactories(): void ->willReturn(sprintf('%s/%s', $directory, $localConfigFilename)); $generatedFactoryAssetPath = __DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'; - $generatedFactory = file_get_contents($generatedFactoryAssetPath); + $generatedFactory = file_get_contents($generatedFactoryAssetPath); assert($generatedFactory !== ''); $this->factoryCompiler @@ -285,7 +285,7 @@ public function testWillDetectAlreadyExistingFactories(): void $this->assertErrorRaised( 'There is already an existing factory class registered for' - .' "LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObject": LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObjectFactory' + . ' "LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObject": LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObjectFactory' ); require $generatedFactoryAssetPath; diff --git a/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php index 85bfcbd8..61829a9b 100644 --- a/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php +++ b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php @@ -1,4 +1,5 @@ factoryCreator = $this->createMock(FactoryCreatorInterface::class); - $this->compiler = new AheadOfTimeFactoryCompiler( + $this->compiler = new AheadOfTimeFactoryCompiler( $this->factoryCreator, ); } - public function testCanHandleConfigWithoutServicesRegisteredWithReflectionBasedAbstractFactory(): void + /** + * @return array + */ + public function configurationsWithoutRegisteredServices(): array + { + return [ + 'empty config' => [ + [], + ], + 'config with integer keys' => [ + [1, 2, 3], + ], + 'config with container config without having registered services' => [ + ['service_manager' => ['factories' => []]], + ], + 'config with non-array config parameters' => [ + ['foo' => 'bar'], + ], + ]; + } + + /** + * @dataProvider configurationsWithoutRegisteredServices + */ + public function testCanHandleConfigWithoutServicesRegisteredWithReflectionBasedAbstractFactory(array $config): void { $this->factoryCreator ->expects(self::never()) ->method(self::anything()); - self::assertSame([], $this->compiler->compile([])); + self::assertSame([], $this->compiler->compile($config)); } public function testCanHandleLaminasMvcServiceManagerConfiguration(): void @@ -75,10 +101,10 @@ public function nonClassReferencingServiceNames(): array 'nonexistent-service-name' => [ 'foobar', ], - 'interface' => [ + 'interface' => [ FactoryInterface::class, ], - 'trait' => [ + 'trait' => [ WhateverTrait::class, ], ]; @@ -148,4 +174,81 @@ public function testWillDetectSameServiceProvidedByMultipleServiceOrPluginManage $this->compiler->compile($config); } + + public function testWillProvideFactoriesForDifferentContainerConfigurations(): void + { + $config = [ + 'foo' => [ + 'factories' => [ + ComplexDependencyObject::class => ReflectionBasedAbstractFactory::class, + ], + ], + 'bar' => [ + 'factories' => [ + SimpleDependencyObject::class => ReflectionBasedAbstractFactory::class, + ], + ], + ]; + + $this->factoryCreator + ->expects(self::exactly(2)) + ->method('createFactory') + ->willReturnMap([ + [ComplexDependencyObject::class, [], 'factory for complex dependency object'], + [SimpleDependencyObject::class, [], 'factory for simple dependency object'], + ]); + + $factories = $this->compiler->compile($config); + self::assertCount(2, $factories); + } + + public function testWillDetectReflectionBasedFactoryInstancesWithClassString(): void + { + $config = [ + 'foo' => [ + 'factories' => [ + ComplexDependencyObject::class => ReflectionBasedAbstractFactory::class, + ], + ], + 'bar' => [ + 'factories' => [ + SimpleDependencyObject::class => new ReflectionBasedAbstractFactory(), + ], + ], + ]; + + $this->factoryCreator + ->expects(self::exactly(2)) + ->method('createFactory') + ->willReturnMap([ + [ComplexDependencyObject::class, [], 'factory for complex dependency object'], + [SimpleDependencyObject::class, [], 'factory for simple dependency object'], + ]); + + $factories = $this->compiler->compile($config); + self::assertCount(2, $factories); + } + + public function testPassesAliasesToFactoryCreator(): void + { + $config = [ + 'dependencies' => [ + 'factories' => [ + stdClass::class => new ReflectionBasedAbstractFactory([ + 'foo' => 'bar', + ]), + ], + ], + ]; + + $this->factoryCreator + ->expects(self::once()) + ->method('createFactory') + ->with(stdClass::class, ['foo' => 'bar']) + ->willReturn('generated factory'); + + $factories = $this->compiler->compile($config); + self::assertCount(1, $factories); + self::assertSame('generated factory', $factories[0]->generatedFactory); + } } diff --git a/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php b/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php index 548b2ace..6f0bffa3 100644 --- a/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php +++ b/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php @@ -1,4 +1,5 @@ Date: Mon, 10 Apr 2023 00:07:58 +0200 Subject: [PATCH 30/32] qa: add enum issues to psalm baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- psalm-baseline.xml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 562d878a..f80e1fcb 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -220,6 +220,11 @@ array + + + enum_exists($service) + + $config['service_manager'] @@ -422,8 +427,6 @@ $instance2 $instance2 $service - $service - $service $serviceFromAlias $serviceFromServiceNameAfterUsingAlias @@ -442,6 +445,21 @@ $name + + + + array<non-empty-string,array{string}> + + + WhateverEnum + + + + + WhateverEnum + case + + $test From 9035a2d89b12a549d28061b0a00feea684612f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 10 Apr 2023 00:31:46 +0200 Subject: [PATCH 31/32] qa: fix line-length coding-standard issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- test/Command/AheadOfTimeFactoryCreatorCommandTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php index 2afed3f8..b5ce594c 100644 --- a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -285,7 +285,8 @@ public function testWillDetectAlreadyExistingFactories(): void $this->assertErrorRaised( 'There is already an existing factory class registered for' - . ' "LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObject": LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObjectFactory' + . ' "LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObject":' + . ' LaminasTest\\ServiceManager\\TestAsset\\SimpleDependencyObjectFactory' ); require $generatedFactoryAssetPath; From 89976b0a8032ea43fe344fb633e677c4fb4c6375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 10 Apr 2023 00:32:49 +0200 Subject: [PATCH 32/32] docs: remove superfluous whitespace at the end of the line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v4/console-tools.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/book/v4/console-tools.md b/docs/book/v4/console-tools.md index 76689519..73a89f51 100644 --- a/docs/book/v4/console-tools.md +++ b/docs/book/v4/console-tools.md @@ -11,6 +11,7 @@ To run the console tools with `laminas-servicemanager` v4, the [`laminas/laminas > ```shell > $ composer require laminas/laminas-cli > ``` +> > _In case laminas-cli is only required to consume these console tools, you might consider using the `--dev` flag._ ## Available Commands @@ -99,4 +100,4 @@ Options: This utility will generate factories in the same way as [servicemanager:generate-factory-for-class](#generate-factory-for-class). The main difference is, that it will scan the whole project configuration for the usage of `ReflectionBasedAbstractFactory` within **any** ServiceManager look-a-like configuration (i.e. explicit usage within `factories`) and auto-generates factories for all of these services **plus** creates a configuration file which overrides **all** ServiceManager look-a-like configurations so that these consume the generated factories. -For more details and how to set up a project so that all factories are properly replaced, refer to the [dedicated command documentation](ahead-of-time-factories.md). +For more details and how to set up a project so that all factories are properly replaced, refer to the [dedicated command documentation](ahead-of-time-factories.md).