diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 24447429..d1100ffe 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -3,8 +3,7 @@ name: docs-build on: release: types: [published] - repository_dispatch: - types: docs-build + workflow_dispatch: jobs: build-deploy: @@ -13,5 +12,5 @@ jobs: - name: Build Docs uses: laminas/documentation-theme/github-actions/docs@master env: - "DOCS_DEPLOY_KEY": ${{ secrets.DOCS_DEPLOY_KEY }} - "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }} + DEPLOY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/benchmarks/BenchAsset/AbstractFactoryFoo.php b/benchmarks/BenchAsset/AbstractFactoryFoo.php index 42cee972..f7b2ecb7 100644 --- a/benchmarks/BenchAsset/AbstractFactoryFoo.php +++ b/benchmarks/BenchAsset/AbstractFactoryFoo.php @@ -10,7 +10,7 @@ class AbstractFactoryFoo implements AbstractFactoryInterface { /** {@inheritDoc} */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { if ($requestedName === 'foo') { return new Foo($options); @@ -19,7 +19,7 @@ public function __invoke(ContainerInterface $container, $requestedName, ?array $ } /** {@inheritDoc} */ - public function canCreate(ContainerInterface $container, $requestedName) + public function canCreate(ContainerInterface $container, string $requestedName): bool { return $requestedName === 'foo'; } diff --git a/benchmarks/BenchAsset/FactoryFoo.php b/benchmarks/BenchAsset/FactoryFoo.php index 7afc5263..2aaf8b53 100644 --- a/benchmarks/BenchAsset/FactoryFoo.php +++ b/benchmarks/BenchAsset/FactoryFoo.php @@ -10,7 +10,7 @@ class FactoryFoo implements FactoryInterface { /** {@inheritDoc} */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { return new Foo($options); } diff --git a/bin/generate-deps-for-config-factory b/bin/generate-deps-for-config-factory deleted file mode 100755 index 8ee1105a..00000000 --- a/bin/generate-deps-for-config-factory +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env php -=7.1" + }, + "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.19.1" + }, + "time": "2024-03-17T08:10:35+00:00" + }, { "name": "psr/container", - "version": "1.1.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -109,9 +219,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:47:00+00:00" } ], "packages-dev": [ @@ -206,16 +316,16 @@ }, { "name": "amphp/byte-stream", - "version": "v1.8.2", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", "shasum": "" }, "require": { @@ -231,6 +341,11 @@ "psalm/phar": "^3.11.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { "files": [ "lib/functions.php" @@ -254,7 +369,7 @@ } ], "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", + "homepage": "http://amphp.org/byte-stream", "keywords": [ "amp", "amphp", @@ -264,8 +379,9 @@ "stream" ], "support": { + "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" }, "funding": [ { @@ -273,7 +389,65 @@ "type": "github" } ], - "time": "2024-04-13T18:00:56+00:00" + "time": "2021-03-30T17:13:30+00:00" + }, + { + "name": "boesing/psalm-plugin-stringf", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/boesing/psalm-plugin-stringf.git", + "reference": "7c3be84584f84e014068c334fe995093a0248bcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/boesing/psalm-plugin-stringf/zipball/7c3be84584f84e014068c334fe995093a0248bcc", + "reference": "7c3be84584f84e014068c334fe995093a0248bcc", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0", + "vimeo/psalm": "^4.30 || ^5.0", + "webmozart/assert": "^1.11" + }, + "require-dev": { + "codeception/codeception": "^4.1", + "codeception/module-asserts": "^2.0", + "codeception/module-phpbrowser": "^2.0", + "doctrine/coding-standard": "^10.0", + "symfony/console": "^5.4", + "symfony/finder": "^5.4", + "symfony/yaml": "^5.4", + "weirdan/codeception-psalm-module": "^0.14.0" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Boesing\\PsalmPluginStringf\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Boesing\\PsalmPluginStringf\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Psalm plugin to work with `sprintf`, `printf`, `sscanf` and `fscanf`.", + "support": { + "issues": "https://github.com/boesing/psalm-plugin-stringf/issues", + "source": "https://github.com/boesing/psalm-plugin-stringf/tree/1.4.0" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-12-10T15:57:20+00:00" }, { "name": "composer/package-versions-deprecated", @@ -350,38 +524,30 @@ }, { "name": "composer/pcre", - "version": "3.3.1", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", - "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, "require-dev": { - "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8 || ^9" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] } }, "autoload": { @@ -409,7 +575,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.1" + "source": "https://github.com/composer/pcre/tree/3.1.3" }, "funding": [ { @@ -425,28 +591,28 @@ "type": "tidelift" } ], - "time": "2024-08-27T18:44:43+00:00" + "time": "2024-03-19T10:26:25+00:00" }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.11", - "symfony/phpunit-bridge": "^3 || ^7" + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { @@ -490,7 +656,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { @@ -506,20 +672,20 @@ "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.5", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", "shasum": "" }, "require": { @@ -556,7 +722,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" }, "funding": [ { @@ -572,7 +738,7 @@ "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2024-03-26T18:29:49+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -688,16 +854,16 @@ }, { "name": "doctrine/annotations", - "version": "2.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", + "reference": "e157ef3f3124bbf6fe7ce0ffd109e8a8ef284e7f", "shasum": "" }, "require": { @@ -709,10 +875,10 @@ "require-dev": { "doctrine/cache": "^2.0", "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.10.28", + "phpstan/phpstan": "^1.8.0", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6.4 || ^7", - "vimeo/psalm": "^4.30 || ^5.14" + "symfony/cache": "^5.4 || ^6", + "vimeo/psalm": "^4.10" }, "suggest": { "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" @@ -758,9 +924,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.2" + "source": "https://github.com/doctrine/annotations/tree/2.0.1" }, - "time": "2024-09-05T10:17:24+00:00" + "time": "2023-02-02T22:02:53+00:00" }, { "name": "doctrine/lexer", @@ -886,16 +1052,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.5.3", + "version": "v1.5.2", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", - "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", "shasum": "" }, "require": { @@ -936,22 +1102,22 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" }, - "time": "2024-04-30T00:40:11+00:00" + "time": "2022-03-02T22:36:06+00:00" }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", "shasum": "" }, "require": { @@ -991,7 +1157,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" }, "funding": [ { @@ -999,7 +1165,7 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2024-02-07T09:43:46+00:00" }, { "name": "friendsofphp/proxy-manager-lts", @@ -1083,18 +1249,86 @@ ], "time": "2024-03-20T12:50:41+00:00" }, + { + "name": "laminas/laminas-cli", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-cli.git", + "reference": "cc59875b2a983b05a70abf4f9b3af739b1257f34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-cli/zipball/cc59875b2a983b05a70abf4f9b3af739b1257f34", + "reference": "cc59875b2a983b05a70abf4f9b3af739b1257f34", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/container": "^1.0 || ^2.0", + "symfony/console": "^6.0 || ^7.0", + "symfony/event-dispatcher": "^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17", + "webmozart/assert": "^1.10" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-mvc": "^3.7.0", + "laminas/laminas-servicemanager": "^3.22.1", + "mikey179/vfsstream": "2.0.x-dev", + "phpunit/phpunit": "^10.5.5", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.18" + }, + "bin": [ + "bin/laminas" + ], + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Cli\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Command-line interface for Laminas projects", + "keywords": [ + "cli", + "command", + "console", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-cli/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/laminas-cli/issues", + "rss": "https://github.com/mezzio/laminas-cli/releases.atom", + "source": "https://github.com/mezzio/laminas-cli" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-01-02T15:08:03+00:00" + }, { "name": "laminas/laminas-code", - "version": "4.14.0", + "version": "4.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-code.git", - "reference": "562e02b7d85cb9142b5116cc76c4c7c162a11a1c" + "reference": "7353d4099ad5388e84737dd16994316a04f48dbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/562e02b7d85cb9142b5116cc76c4c7c162a11a1c", - "reference": "562e02b7d85cb9142b5116cc76c4c7c162a11a1c", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/7353d4099ad5388e84737dd16994316a04f48dbf", + "reference": "7353d4099ad5388e84737dd16994316a04f48dbf", "shasum": "" }, "require": { @@ -1106,7 +1340,7 @@ "laminas/laminas-coding-standard": "^2.5.0", "laminas/laminas-stdlib": "^3.17.0", "phpunit/phpunit": "^10.3.3", - "psalm/plugin-phpunit": "^0.19.0", + "psalm/plugin-phpunit": "^0.18.4", "vimeo/psalm": "^5.15.0" }, "suggest": { @@ -1144,7 +1378,7 @@ "type": "community_bridge" } ], - "time": "2024-06-17T08:50:25+00:00" + "time": "2023-10-18T10:00:55+00:00" }, { "name": "laminas/laminas-coding-standard", @@ -1204,30 +1438,30 @@ }, { "name": "laminas/laminas-container-config-test", - "version": "0.8.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-container-config-test.git", - "reference": "06474faed18a2732b21355297fa8c56f1aff2e91" + "reference": "8e91d957055f0b81b86967d255316d63dbb55053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-container-config-test/zipball/06474faed18a2732b21355297fa8c56f1aff2e91", - "reference": "06474faed18a2732b21355297fa8c56f1aff2e91", + "url": "https://api.github.com/repos/laminas/laminas-container-config-test/zipball/8e91d957055f0b81b86967d255316d63dbb55053", + "reference": "8e91d957055f0b81b86967d255316d63dbb55053", "shasum": "" }, "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0", - "psr/container": "^1.0 || ^2.0" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "phpunit/phpunit": "^10.3", + "psr/container": "^1.0 || ^2.0.2" }, "conflict": { "zendframework/zend-container-config-test": "*" }, "require-dev": { - "laminas/laminas-coding-standard": "^2.3", - "phpunit/phpunit": "^9.5.26", - "psalm/plugin-phpunit": "^0.18.0", - "vimeo/psalm": "^4.29.0" + "laminas/laminas-coding-standard": "^2.5", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.15.0" }, "type": "library", "autoload": { @@ -1267,28 +1501,105 @@ "type": "community_bridge" } ], - "time": "2022-11-16T00:42:21+00:00" + "time": "2023-12-05T11:09:25+00:00" + }, + { + "name": "lctrs/psalm-psr-container-plugin", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/Lctrs/psalm-psr-container-plugin.git", + "reference": "3cdebca2e6bbe95481a06abb60d4f2f3ac9a849d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Lctrs/psalm-psr-container-plugin/zipball/3cdebca2e6bbe95481a06abb60d4f2f3ac9a849d", + "reference": "3cdebca2e6bbe95481a06abb60d4f2f3ac9a849d", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "nikic/php-parser": "^4.18.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/container": "^1.1.2 || ^2.0.2", + "vimeo/psalm": "^5.19.0" + }, + "require-dev": { + "codeception/codeception": "5.0.13", + "codeception/module-asserts": "3.0.0", + "codeception/module-cli": "2.0.1", + "codeception/module-filesystem": "3.0.1", + "doctrine/coding-standard": "12.0.0", + "ergebnis/composer-normalize": "2.41.1", + "ergebnis/license": "2.4.0", + "maglnet/composer-require-checker": "4.7.1", + "phpstan/extension-installer": "1.3.1", + "phpstan/phpstan": "1.10.55", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.15", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "10.5.5", + "psalm/plugin-phpunit": "0.18.4", + "weirdan/codeception-psalm-module": "0.14.0" + }, + "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": "2024-01-10T18:59:28+00:00" }, { "name": "mikey179/vfsstream", - "version": "v1.6.12", + "version": "v1.6.11", "source": { "type": "git", "url": "https://github.com/bovigo/vfsStream.git", - "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", - "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", "shasum": "" }, "require": { - "php": ">=7.1.0" + "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^7.5||^8.5||^9.6", - "yoast/phpunit-polyfills": "^2.0" + "phpunit/phpunit": "^4.5|^5.0" }, "type": "library", "extra": { @@ -1319,20 +1630,20 @@ "source": "https://github.com/bovigo/vfsStream/tree/master", "wiki": "https://github.com/bovigo/vfsStream/wiki" }, - "time": "2024-08-29T18:43:31+00:00" + "time": "2022-02-23T02:02:42+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { @@ -1340,12 +1651,11 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" + "doctrine/common": "<2.13.3 || >=3,<3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -1371,7 +1681,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -1379,20 +1689,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v4.5.0", + "version": "v4.4.1", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", "shasum": "" }, "require": { @@ -1421,72 +1731,16 @@ "name": "Christian Weiske", "email": "cweiske@cweiske.de", "homepage": "http://github.com/cweiske/jsonmapper/", - "role": "Developer" - } - ], - "description": "Map nested JSON structures onto PHP classes", - "support": { - "email": "cweiske@cweiske.de", - "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" - }, - "time": "2024-09-08T10:13:13+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.19.4", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.1" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^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" + "role": "Developer" + } ], + "description": "Map nested JSON structures onto PHP classes", "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" }, - "time": "2024-09-29T15:01:53+00:00" + "time": "2024-01-31T06:18:54+00:00" }, { "name": "phar-io/manifest", @@ -1706,21 +1960,20 @@ "issues": "https://github.com/phpbench/dom/issues", "source": "https://github.com/phpbench/dom/tree/0.3.3" }, - "abandoned": true, "time": "2023-03-06T23:46:57+00:00" }, { "name": "phpbench/phpbench", - "version": "1.3.1", + "version": "1.2.15", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0" + "reference": "f7000319695cfad04a57fc64bf7ef7abdf4c437c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0", - "reference": "a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/f7000319695cfad04a57fc64bf7ef7abdf4c437c", + "reference": "f7000319695cfad04a57fc64bf7ef7abdf4c437c", "shasum": "" }, "require": { @@ -1732,30 +1985,29 @@ "ext-spl": "*", "ext-tokenizer": "*", "php": "^8.1", - "phpbench/container": "^2.2", + "phpbench/container": "^2.1", "phpbench/dom": "~0.3.3", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", - "symfony/console": "^6.1 || ^7.0", - "symfony/filesystem": "^6.1 || ^7.0", - "symfony/finder": "^6.1 || ^7.0", - "symfony/options-resolver": "^6.1 || ^7.0", - "symfony/process": "^6.1 || ^7.0", + "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/finder": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/process": "^4.2 || ^5.0 || ^6.0 || ^7.0", "webmozart/glob": "^4.6" }, "require-dev": { "dantleech/invoke": "^2.0", - "ergebnis/composer-normalize": "^2.39", "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^1.0", "phpspec/prophecy": "dev-master", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.4", - "rector/rector": "^0.18.11 || ^1.0.0", - "symfony/error-handler": "^6.1 || ^7.0", - "symfony/var-dumper": "^6.1 || ^7.0" + "phpunit/phpunit": "^10.0", + "rector/rector": "^0.18.10", + "symfony/error-handler": "^5.2 || ^6.0 || ^7.0", + "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-xdebug": "For Xdebug profiling extension." @@ -1798,7 +2050,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.3.1" + "source": "https://github.com/phpbench/phpbench/tree/1.2.15" }, "funding": [ { @@ -1806,7 +2058,7 @@ "type": "github" } ], - "time": "2024-06-30T11:04:37+00:00" + "time": "2023-11-29T12:21:11+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2019,32 +2271,32 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.16", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-text-template": "^3.0.1", - "sebastian/code-unit-reverse-lookup": "^3.0.0", - "sebastian/complexity": "^3.2.0", - "sebastian/environment": "^6.1.0", - "sebastian/lines-of-code": "^2.0.2", - "sebastian/version": "^4.0.1", - "theseer/tokenizer": "^1.2.3" + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", + "theseer/tokenizer": "^1.2.0" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -2056,7 +2308,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1.x-dev" + "dev-main": "10.1-dev" } }, "autoload": { @@ -2085,7 +2337,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -2093,7 +2345,7 @@ "type": "github" } ], - "time": "2024-08-22T04:31:57+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2340,16 +2592,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.36", + "version": "10.5.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870" + "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", - "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd", + "reference": "18f8d4a5f52b61fdd9370aaae3167daa0eeb69cd", "shasum": "" }, "require": { @@ -2359,26 +2611,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.16", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-invoker": "^4.0.0", - "phpunit/php-text-template": "^3.0.1", - "phpunit/php-timer": "^6.0.0", - "sebastian/cli-parser": "^2.0.1", - "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.2", - "sebastian/diff": "^5.1.1", - "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", - "sebastian/global-state": "^6.0.2", - "sebastian/object-enumerator": "^5.0.0", - "sebastian/recursion-context": "^5.0.0", - "sebastian/type": "^4.0.0", - "sebastian/version": "^4.0.1" + "phpunit/php-code-coverage": "^10.1.5", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.1", + "sebastian/global-state": "^6.0.1", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -2421,7 +2673,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.36" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.16" }, "funding": [ { @@ -2437,7 +2689,7 @@ "type": "tidelift" } ], - "time": "2024-10-08T15:36:51+00:00" + "time": "2024-03-28T10:08:10+00:00" }, { "name": "psalm/plugin-phpunit", @@ -2548,18 +2800,68 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/log", - "version": "3.0.2", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", "shasum": "" }, "require": { @@ -2594,9 +2896,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "source": "https://github.com/php-fig/log/tree/3.0.0" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2021-07-14T16:46:02+00:00" }, { "name": "sebastian/cli-parser", @@ -2768,16 +3070,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.2", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53" + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", - "reference": "2d3e04c3b4c1e84a5e7382221ad8883c8fbc4f53", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", "shasum": "" }, "require": { @@ -2788,7 +3090,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.4" + "phpunit/phpunit": "^10.3" }, "type": "library", "extra": { @@ -2833,7 +3135,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" }, "funding": [ { @@ -2841,7 +3143,7 @@ "type": "github" } ], - "time": "2024-08-12T06:03:08+00:00" + "time": "2023-08-14T13:18:12+00:00" }, { "name": "sebastian/complexity", @@ -3516,23 +3818,23 @@ }, { "name": "seld/jsonlint", - "version": "1.11.0", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", "shasum": "" }, "require": { "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.11", + "phpstan/phpstan": "^1.5", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, "bin": [ @@ -3564,7 +3866,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" }, "funding": [ { @@ -3576,7 +3878,7 @@ "type": "tidelift" } ], - "time": "2024-07-11T14:55:45+00:00" + "time": "2024-02-07T12:57:50+00:00" }, { "name": "slevomat/coding-standard", @@ -3641,16 +3943,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.3.0", + "version": "3.2.3", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876" + "reference": "c95fd4db94ec199f798d4b5b4a81757bd20d88ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/c95fd4db94ec199f798d4b5b4a81757bd20d88ab", + "reference": "c95fd4db94ec199f798d4b5b4a81757bd20d88ab", "shasum": "" }, "require": { @@ -3663,11 +3965,6 @@ "spatie/pest-plugin-snapshots": "^1.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { "psr-4": { "Spatie\\ArrayToXml\\": "src" @@ -3693,7 +3990,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.3.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.2.3" }, "funding": [ { @@ -3705,20 +4002,20 @@ "type": "github" } ], - "time": "2024-05-01T10:20:27+00:00" + "time": "2024-02-07T10:39:02+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.3", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", "shasum": "" }, "require": { @@ -3785,20 +4082,20 @@ "type": "open_collective" } ], - "time": "2024-09-18T10:38:58+00:00" + "time": "2024-03-31T21:03:09+00:00" }, { "name": "symfony/console", - "version": "v6.4.12", + "version": "v6.4.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", - "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", + "url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f", + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f", "shasum": "" }, "require": { @@ -3863,7 +4160,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.12" + "source": "https://github.com/symfony/console/tree/v6.4.6" }, "funding": [ { @@ -3879,20 +4176,20 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:15:52+00:00" + "time": "2024-03-29T19:07:53+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -3901,7 +4198,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3930,7 +4227,163 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "4e64b49bf370ade88e567de29465762e316e4224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4e64b49bf370ade88e567de29465762e316e4224", + "reference": "4e64b49bf370ade88e567de29465762e316e4224", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.2" }, "funding": [ { @@ -3946,20 +4399,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-01-23T14:51:35+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.12", + "version": "v6.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12" + "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/f810e3cbdf7fdc35983968523d09f349fa9ada12", - "reference": "f810e3cbdf7fdc35983968523d09f349fa9ada12", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/9919b5509ada52cc7f66f9a35c86a4a29955c9d3", + "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3", "shasum": "" }, "require": { @@ -3967,9 +4420,6 @@ "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, - "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" - }, "type": "library", "autoload": { "psr-4": { @@ -3996,7 +4446,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.12" + "source": "https://github.com/symfony/filesystem/tree/v6.4.6" }, "funding": [ { @@ -4012,20 +4462,20 @@ "type": "tidelift" } ], - "time": "2024-09-16T16:01:33+00:00" + "time": "2024-03-21T19:36:20+00:00" }, { "name": "symfony/finder", - "version": "v6.4.11", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453" + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d7eb6daf8cd7e9ac4976e9576b32042ef7253453", - "reference": "d7eb6daf8cd7e9ac4976e9576b32042ef7253453", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", "shasum": "" }, "require": { @@ -4060,7 +4510,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.11" + "source": "https://github.com/symfony/finder/tree/v6.4.0" }, "funding": [ { @@ -4076,20 +4526,20 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:27:37+00:00" + "time": "2023-10-31T17:30:12+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.4.8", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" + "reference": "22301f0e7fdeaacc14318928612dee79be99860e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e", "shasum": "" }, "require": { @@ -4127,7 +4577,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.8" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" }, "funding": [ { @@ -4143,24 +4593,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2023-08-08T10:16:24+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "provide": { "ext-ctype": "*" @@ -4206,7 +4656,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -4222,24 +4672,24 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -4284,7 +4734,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" }, "funding": [ { @@ -4300,24 +4750,24 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -4365,7 +4815,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" }, "funding": [ { @@ -4381,24 +4831,24 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "provide": { "ext-mbstring": "*" @@ -4445,7 +4895,87 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -4461,20 +4991,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/process", - "version": "v6.4.12", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3" + "reference": "710e27879e9be3395de2b98da3f52a946039f297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/3f94e5f13ff58df371a7ead461b6e8068900fbb3", - "reference": "3f94e5f13ff58df371a7ead461b6e8068900fbb3", + "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", + "reference": "710e27879e9be3395de2b98da3f52a946039f297", "shasum": "" }, "require": { @@ -4506,7 +5036,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.12" + "source": "https://github.com/symfony/process/tree/v6.4.4" }, "funding": [ { @@ -4522,26 +5052,25 @@ "type": "tidelift" } ], - "time": "2024-09-17T12:47:12+00:00" + "time": "2024-02-20T12:31:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" + "psr/container": "^1.1|^2.0" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -4549,7 +5078,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -4589,7 +5118,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" }, "funding": [ { @@ -4605,20 +5134,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2023-12-19T21:51:00+00:00" }, { "name": "symfony/string", - "version": "v6.4.12", + "version": "v6.4.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b" + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f8a1ccebd0997e16112dfecfd74220b78e5b284b", - "reference": "f8a1ccebd0997e16112dfecfd74220b78e5b284b", + "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", "shasum": "" }, "require": { @@ -4675,7 +5204,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.12" + "source": "https://github.com/symfony/string/tree/v6.4.4" }, "funding": [ { @@ -4691,7 +5220,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:15:52+00:00" + "time": "2024-02-01T13:16:41+00:00" }, { "name": "theseer/tokenizer", @@ -4745,16 +5274,16 @@ }, { "name": "vimeo/psalm", - "version": "5.26.1", + "version": "5.23.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" + "reference": "8471a896ccea3526b26d082f4461eeea467f10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/8471a896ccea3526b26d082f4461eeea467f10a4", + "reference": "8471a896ccea3526b26d082f4461eeea467f10a4", "shasum": "" }, "require": { @@ -4775,7 +5304,7 @@ "felixfbecker/language-server-protocol": "^1.5.2", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.17", + "nikic/php-parser": "^4.16", "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", @@ -4851,25 +5380,25 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-09-08T18:53:08+00:00" + "time": "2024-03-11T20:33:46+00:00" }, { "name": "webimpress/coding-standard", - "version": "1.4.0", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/webimpress/coding-standard.git", - "reference": "6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53" + "reference": "710f71ac95d36d931e76b47132b599c39abfab11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webimpress/coding-standard/zipball/6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53", - "reference": "6f6a1a90bd9e18fc8bee0660dd1d1ce68cf9fc53", + "url": "https://api.github.com/repos/webimpress/coding-standard/zipball/710f71ac95d36d931e76b47132b599c39abfab11", + "reference": "710f71ac95d36d931e76b47132b599c39abfab11", "shasum": "" }, "require": { "php": "^7.3 || ^8.0", - "squizlabs/php_codesniffer": "^3.10.3" + "squizlabs/php_codesniffer": "^3.7.2" }, "require-dev": { "phpunit/phpunit": "^9.6.15" @@ -4898,7 +5427,7 @@ ], "support": { "issues": "https://github.com/webimpress/coding-standard/issues", - "source": "https://github.com/webimpress/coding-standard/tree/1.4.0" + "source": "https://github.com/webimpress/coding-standard/tree/1.3.2" }, "funding": [ { @@ -4906,7 +5435,7 @@ "type": "github" } ], - "time": "2024-10-16T06:55:17+00:00" + "time": "2023-12-18T07:25:41+00:00" }, { "name": "webmozart/assert", @@ -5018,15 +5547,17 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "mikey179/vfsstream": 15 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" }, "platform-dev": [], "platform-overrides": { "php": "8.1.99" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/docs/book/index.html b/docs/book/index.html deleted file mode 100644 index 5c00f74d..00000000 --- a/docs/book/index.html +++ /dev/null @@ -1,10 +0,0 @@ -
-
-

laminas-servicemanager

- -

Factory-Driven Dependency Injection Container

- -
$ composer require laminas/laminas-servicemanager
-
-
- 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/index.md b/docs/book/index.md new file mode 100644 index 00000000..86a84e30 --- /dev/null +++ b/docs/book/index.md @@ -0,0 +1,3 @@ +# This Is Only a Placeholder + +The content of this page is automatically generated. 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/lazy-services.md b/docs/book/v3/lazy-services.md similarity index 100% rename from docs/book/lazy-services.md rename to docs/book/v3/lazy-services.md diff --git a/docs/book/migration.md b/docs/book/v3/migration.md similarity index 99% rename from docs/book/migration.md rename to docs/book/v3/migration.md index cfeea238..20b70a95 100644 --- a/docs/book/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 diff --git a/docs/book/plugin-managers.md b/docs/book/v3/plugin-managers.md similarity index 100% rename from docs/book/plugin-managers.md rename to docs/book/v3/plugin-managers.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/reflection-abstract-factory.md b/docs/book/v3/reflection-abstract-factory.md similarity index 100% rename from docs/book/reflection-abstract-factory.md rename to docs/book/v3/reflection-abstract-factory.md diff --git a/docs/book/v4/cli-commands/generate-ahead-of-time-factories.md b/docs/book/v4/cli-commands/generate-ahead-of-time-factories.md new file mode 100644 index 00000000..00a228d7 --- /dev/null +++ b/docs/book/v4/cli-commands/generate-ahead-of-time-factories.md @@ -0,0 +1,103 @@ +# Ahead of Time Factories + +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](introduction.md). + +## 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): + +```shell +$ 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. + +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. + +### 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. + +```shell +$ 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: + +```text +. +├── 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: + +```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/docs/book/v4/cli-commands/generate-dependencies-for-config-factory.md b/docs/book/v4/cli-commands/generate-dependencies-for-config-factory.md new file mode 100644 index 00000000..cc0f5cf8 --- /dev/null +++ b/docs/book/v4/cli-commands/generate-dependencies-for-config-factory.md @@ -0,0 +1,31 @@ +# Generate Dependencies for Config Factory + +```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 +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. + +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. \ No newline at end of file diff --git a/docs/book/v4/cli-commands/generate-factory-for-class.md b/docs/book/v4/cli-commands/generate-factory-for-class.md new file mode 100644 index 00000000..399cbcce --- /dev/null +++ b/docs/book/v4/cli-commands/generate-factory-for-class.md @@ -0,0 +1,28 @@ +# Generate Factory for Class + +```bash +$ ./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: + servicemanager:generate-factory-for-class + +Arguments: + className Name of the class to reflect and for which to generate a factory. + +Options: + -q, --quiet Do not output any message +``` + +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/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. \ No newline at end of file diff --git a/docs/book/v4/cli-commands/introduction.md b/docs/book/v4/cli-commands/introduction.md new file mode 100644 index 00000000..2b50f68a --- /dev/null +++ b/docs/book/v4/cli-commands/introduction.md @@ -0,0 +1,24 @@ +# Introduction + +INFO: +Starting in 4.0.0, `laminas-servicemanager` moved the CLI tooling to [`laminas-cli`](https://docs.laminas.dev/laminas-cli/) and provides several commands to be executed. + +> MISSING: **Installation Requirements** +> +> To run the console tools with `laminas-servicemanager`, the `laminas/laminas-cli` component needs to be added to the project dependencies. +> +> ```bash +> $ 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 + +- [Generate Dependencies for Config Factory](generate-dependencies-for-config-factory.md) +- [Generate Factory for Class](generate-factory-for-class.md) +- [Generate Ahead of Time Factories](generate-ahead-of-time-factories.md) + +## Learn More + +- [laminas-cli: Writing Custom Commands for laminas-mvc and Mezzio based Applications](https://docs.laminas.dev/laminas-cli/) \ No newline at end of file diff --git a/docs/book/v4/config-abstract-factory.md b/docs/book/v4/config-abstract-factory.md new file mode 100644 index 00000000..690815c6 --- /dev/null +++ b/docs/book/v4/config-abstract-factory.md @@ -0,0 +1,142 @@ +# Config Abstract Factory + +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, 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..63347691 --- /dev/null +++ b/docs/book/v4/configuring-the-service-manager.md @@ -0,0 +1,593 @@ +# 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 + +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 older versions of this component. + +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/cookbook/factories-vs-abstract-factories.md b/docs/book/v4/cookbook/factories-vs-abstract-factories.md new file mode 100644 index 00000000..42f284dd --- /dev/null +++ b/docs/book/v4/cookbook/factories-vs-abstract-factories.md @@ -0,0 +1,98 @@ +# When To Use Factories vs Abstract Factories + +`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 boilerplate 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 + +We are offering a variety of [CLI commands](../cli-commands/introduction.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/lazy-services.md b/docs/book/v4/lazy-services.md new file mode 100644 index 00000000..3f24327c --- /dev/null +++ b/docs/book/v4/lazy-services.md @@ -0,0 +1,146 @@ +# Lazy Services + +`Laminas\ServiceManager` can use [delegator factories](delegators.md) to generate +"lazy" references to your services. + +Lazy services are [proxies](http://en.wikipedia.org/wiki/Proxy_pattern) that +get lazily instantiated, and keep a reference to the real instance of +the proxied service. + +## Use cases + +You may want to lazily initialize a service when it is instantiated very often, +but not always used. + +A typical example is a database connection: it is a dependency to many other +elements in your application, but that doesn't mean that every request will +execute queries through it. + +Additionally, instantiating a connection to the database may require some time +and eat up resources. + +Proxying the database connection would allow you to delay that overhead until the +object is really needed. + +## Setup + +`Laminas\ServiceManager\Proxy\LazyServiceFactory` is a [delegator factory](delegators.md) +capable of generating lazy loading proxies for your services. + +The lazy service facilities depend on [ProxyManager](https://github.com/FriendsOfPHP/proxy-manager-lts); +you will need to install that package before using the feature: + +```php +$ composer require friendsofphp/proxy-manager-lts +``` + +## Practical example + +To demonstrate how a lazy service works, you may use the following `Buzzer` +example class, which is designed to be slow at instantiation time for +demonstration purposes: + +```php +namespace MyApp; + +class Buzzer +{ + public function __construct() + { + // deliberately halting the application for 5 seconds + sleep(5); + } + + public function buzz() + { + return 'Buzz!'; + } +} +``` + +You can then proceed and configure the service manager to generate proxies +instead of real services: + +```php +use MyApp\Buzzer; +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\Proxy\LazyServiceFactory; +use Laminas\ServiceManager\ServiceManager; + +$serviceManager = new \Laminas\ServiceManager\ServiceManager([ + 'factories' => [ + Buzzer::class => InvokableFactory::class, + ], + 'lazy_services' => [ + // Mapping services to their class names is required + // since the ServiceManager is not a declarative DIC. + 'class_map' => [ + Buzzer::class => Buzzer::class, + ], + ], + 'delegators' => [ + Buzzer::class => [ + LazyServiceFactory::class, + ], + ], +]); +``` + +This configuration tells the service manager to add the add +`LazyServiceFactory` as a delegator for `Buzzer`. + +You can now retrieve the buzzer: + +```php +use MyApp\Buzzer; + +$buzzer = $serviceManager->get(Buzzer::class); +echo $buzzer->buzz(); +``` + +To verify that the proxying occurred correctly, you can run the following code, +which should delay the 5 seconds wait time hardcoded in `Buzzer::__construct` +until `Buzzer::buzz` is invoked: + +```php +use MyApp\Buzzer; + +for ($i = 0; $i < 100; $i += 1) { + $buzzer = $serviceManager->get(Buzzer::class); + echo "created buzzer $i\n"; +} + +echo $buzzer->buzz(); +``` + +## Configuration + +This is the config structure expected by `Laminas\ServiceManager\Proxy\LazyServiceFactory`, +in the `lazy_services` key passed in the service manager configuration: + +```php +[ + // map of service names and their relative class names - this + // is required since the service manager cannot know the + // class name of defined services up front + 'class_map' => [ + // 'foo' => 'MyApplication\Foo', + ], + + // directory where proxy classes will be written - default to system_get_tmp_dir() + 'proxies_target_dir' => null, + + // namespace of the generated proxies, default to "ProxyManagerGeneratedProxy" + 'proxies_namespace' => null, + + // whether the generated proxy classes should be written to disk or generated on-the-fly + 'write_proxy_files' => false, +]; +``` + +After you have an instance, you can map lazy service/class pairs using +`mapLazyService()`: + +```php +$container->mapLazyService('foo', \MyApplication\Foo::class); +``` diff --git a/docs/book/v4/migration/v3-to-v4.md b/docs/book/v4/migration/v3-to-v4.md new file mode 100644 index 00000000..1fbd9536 --- /dev/null +++ b/docs/book/v4/migration/v3-to-v4.md @@ -0,0 +1,5 @@ +# Migration from Version 3 to 4 + +INFO: **Work in progress** +We are currently working on the migration guide. +We will update this page as soon as possible! diff --git a/docs/book/v4/plugin-managers.md b/docs/book/v4/plugin-managers.md new file mode 100644 index 00000000..e47379f9 --- /dev/null +++ b/docs/book/v4/plugin-managers.md @@ -0,0 +1,92 @@ +# Plugin managers + +Plugin managers are *specialized* service managers, typically used to create +homogeneous objects of a specific type. + +Because a plugin manager extends a service manager, it works the same and can +be configured similarly. It provides a separation of concerns (it will be used +in specific contexts), and provides additional instance validation. + +Laminas components extensively use plugin managers to create services +that share common functionalities. For instance, all validator services are +specified inside a specialized `ValidatorPluginManager`. + +## Creating a plugin manager + +To create a plugin manager, you first need to create a new class that extends +`Laminas\ServiceManager\AbstractPluginManager`: + +```php +class ValidatorPluginManager extends AbstractPluginManager +{ + protected $instanceOf = ValidatorInterface::class; +} +``` + +The `$instanceOf` variable specifies a class/interface type that all instances +retrieved from the plugin manager must fulfill. If an instance created by the +plugin manager does not match, a `Laminas\ServiceManager\Exception\InvalidServiceException` +exception will be thrown. + +Most of the time, this shortcut is enough. However if you have more complex +validation rules, you can override the `validate()` method: + +```php +class ValidatorPluginManager extends AbstractPluginManager +{ + public function validate($instance) + { + if ($instance instanceof Foo || $instance instanceof Bar) { + return; + } + + throw new InvalidServiceException('This is not a valid service!'); + } +} +``` + +## Configuring a plugin manager + +A plugin manager requires that you pass a parent service manager (typically, +the application's service manager) as well as service configuration. Service +configuration follows the exact same pattern as for a normal service manager; +refer to the [configuring the service manager](configuring-the-service-manager.md) section for details. + +Because a plugin manager is often a service itself, we recommend you to +register the plugin manager as part of the general service manager, as shown +below: + +```php +$serviceManager = new ServiceManager([ + 'factories' => [ + ValidatorPluginManager::class => function(ContainerInterface $container, $requestedName) { + return new ValidatorPluginManager($container, [ + 'factories' => [ + StringLengthValidator::class => InvokableFactory::class, + ], + ]); + }, + ], +]); + +// Get the plugin manager: + +$pluginManager = $serviceManager->get(ValidatorPluginManager::class); + +// Use the plugin manager + +$validator = $pluginManager->get(StringLengthValidator::class); +``` + +> When inside the context of the factory +> of a service created by a plugin manager, the passed container **will not +> be** the plugin manager, but the parent service manager instead. If you need +> access to other plugins of the same type, you will need to fetch the plugin +> manager from the container: +> +> ```php +> function ($container, $name, array $options = []) { +> $validators = $container->get(ValidatorPluginManager::class); +> // ... +> } +> ``` diff --git a/docs/book/v4/psr-11.md b/docs/book/v4/psr-11.md new file mode 100644 index 00000000..5be00603 --- /dev/null +++ b/docs/book/v4/psr-11.md @@ -0,0 +1,10 @@ +# PSR-11 Support + +## Standard Support + +Version 4.0 of laminas-servicemanager supports version 1.1 and 2 of [PSR-11: Container interface](https://www.php-fig.org/psr/psr-11/), and has update the various factory interfaces and exception implementations to typehint against the PSR-11 interfaces. + +## Migrating Code to laminas-servicemanager 4.x Compatibility + +To migrate code to be compatible with laminas-servicemanager 4.x, [laminas-servicemanager-migration](https://docs.laminas.dev/laminas-servicemanager-migration/) can be used. +This package provides a set of rules based on Rector that can be used to migrate code. 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/v4/reflection-abstract-factory.md b/docs/book/v4/reflection-abstract-factory.md new file mode 100644 index 00000000..20af5f5a --- /dev/null +++ b/docs/book/v4/reflection-abstract-factory.md @@ -0,0 +1,67 @@ +# Reflection Factory + +Writing a factory class for each and every service that has dependencies +can be tedious, particularly in early development as you are still sorting +out dependencies. + +laminas-servicemanager ships with `Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory`, +which provides a reflection-based approach to instantiation, resolving +constructor dependencies to the relevant services. The factory may be used as +either an abstract factory, or mapped to specific service names as a factory: + +```php +use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; + +return [ + /* ... */ + 'service_manager' => [ + 'abstract_factories' => [ + ReflectionBasedAbstractFactory::class, + ], + 'factories' => [ + 'MyModule\Model\FooModel' => ReflectionBasedAbstractFactory::class, + ], + ], + /* ... */ +]; +``` + +Mapping services to the factory is more explicit and even more performant than in v3.0 due to the [ahead of time factory generation](cli-commands/generate-ahead-of-time-factories.md). + +The factory operates with the following constraints/features: + +- A parameter named `$config` typehinted as an array will receive the + application "config" service (i.e., the merged configuration). +- Parameters typehinted against array, but not named `$config`, will + be injected with an empty array. +- Scalar parameters will result in the factory raising an exception, + unless a default value is present; if it is, that value will be used. +- If a service cannot be found for a given typehint, the factory will + raise an exception detailing this. + +`$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 providing a dedicated +factory, as reflection introduces a performance overhead. + +There are two ways to provide dedicated factories for services consuming `ReflectionBasedAbstractFactory`: + +1. Usage of the [generate-factory-for-class console tool](cli-commands/generate-factory-for-class.md) (this will also require to manually modify the configuration) +2. Usage of the [generate-aot-factories console tool](cli-commands/generate-ahead-of-time-factories.md) which needs an initial project + deployment setup + +## Alternatives + +You may also use the [Config Abstract Factory](config-abstract-factory.md), +which gives slightly more flexibility in terms of mapping dependencies: + +- If you wanted to map to a specific implementation, choose the + `ConfigAbstractFactory`. +- If you need to map to a service that will return a scalar or array (e.g., a + subset of the `'config'` service), choose the `ConfigAbstractFactory`. +- If you need a faster factory for production, choose the + `ConfigAbstractFactory` or create a custom factory. + +## References + +This feature was inspired by [a blog post by Alexandre Lemaire](http://circlical.com/blog/2016/3/9/preparing-for-zend-f). diff --git a/mkdocs.yml b/mkdocs.yml index 35bad2f6..44f7f0cb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,22 +1,61 @@ docs_dir: docs/book site_dir: docs/html nav: - - Home: index.md - - 'Quick Start': quick-start.md + - Home: index.md + - v4: + - "Quick Start": v4/quick-start.md + - "PSR-11 Support": v4/psr-11.md + - "Configuring the service manager": v4/configuring-the-service-manager.md + - Delegators: v4/delegators.md + - "Lazy services": v4/lazy-services.md + - "Plugin managers": v4/plugin-managers.md + - "Autowiring & Configuration-based Factories": + - "Reflection-based Factory": v4/reflection-abstract-factory.md + - "Configuration-based Factory": v4/config-abstract-factory.md + - "CLI Commands": + - Introduction: v4/cli-commands/introduction.md + - "Generate Dependencies for Config Factory": v4/cli-commands/generate-dependencies-for-config-factory.md + - "Generate Factory for Class": v4/cli-commands/generate-factory-for-class.md + - "Generate Ahead of Time Factories": v4/cli-commands/generate-ahead-of-time-factories.md + - Cookbook: + - "Factories vs Abstract Factories": v4/cookbook/factories-vs-abstract-factories.md + - "Migration Guide": + - "Migration from Version 3 to 4": v4/migration/v3-to-v4.md + - v3: + - "Quick Start": v3/quick-start.md - Reference: - - 'PSR-11 Support': psr-11.md - - 'Configuring the service manager': configuring-the-service-manager.md - - Delegators: delegators.md - - '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 - - 'Console Tools': console-tools.md + - "PSR-11 Support": v3/psr-11.md + - "Configuring the service manager": v3/configuring-the-service-manager.md + - Delegators: v3/delegators.md + - "Lazy services": v3/lazy-services.md + - "Plugin managers": v3/plugin-managers.md + - "Configuration-based Abstract Factory": v3/config-abstract-factory.md + - "Reflection-based Abstract Factory": v3/reflection-abstract-factory.md + - "Console Tools": v3/console-tools.md - Cookbook: - - 'Factories vs Abstract Factories': cookbook/factories-vs-abstract-factories.md - - 'Migration Guide': migration.md + - "Factories vs Abstract Factories": v3/cookbook/factories-vs-abstract-factories.md + - "Migration Guide": v3/migration.md site_name: laminas-servicemanager -site_description: 'laminas-servicemanager: factory-driven dependency injection container' -repo_url: 'https://github.com/laminas/laminas-servicemanager' +site_description: "laminas-servicemanager: factory-driven dependency injection container" +repo_url: "https://github.com/laminas/laminas-servicemanager" extra: project: Components + current_version: v4 + versions: + - v4 + - v3 +plugins: + - search + - redirects: + redirect_maps: + quick-start.md: v4/quick-start.md + psr-11.md: v4/psr-11.md + configuring-the-service-manager.md: v4/configuring-the-service-manager.md + delegators.md: v4/delegators.md + lazy-services.md: v4/lazy-services.md + plugin-managers.md: v4/plugin-managers.md + config-abstract-factory.md: v4/config-abstract-factory.md + reflection-abstract-factory.md: v4/reflection-abstract-factory.md + cookbook/factories-vs-abstract-factories.md: v4/cookbook/factories-vs-abstract-factories.md + migration.md: v4/migration/v3-to-v4.md + console-tools.md: v3/console-tools.md diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a8083863..7b6acc8c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -13,7 +13,6 @@ benchmarks - bin src test test/TestAsset/laminas-code/*.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5b61e368..086fcea0 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -11,117 +11,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - allowedKeys[$key]]]> - - - - - - - - - - - - - + - - - - - @@ -132,53 +39,26 @@ - + - - - - - - + - - - - - [$name => [$factory]]]]]> - [$initializer]]]]> - ['class_map' => [$name => $class ?: $name]]]]]> - services[$service]]]> - - factories]]> - - - - - - - - - - - - @@ -193,64 +73,24 @@ - - services[$service]]]> - - - - - - - aliases]]> - factories]]> - shared]]> - sharedByDefault]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -258,33 +98,8 @@ getServiceNotFoundException()]]> getServiceNotFoundException()]]> - - - - - - - - - - - - - - - - - - - - - - - - - @@ -299,182 +114,24 @@ - - - - - - - - - - - - - - - class]]> - class]]> - config]]> - configFile]]> - configFile]]> - ignoreUnresolved]]> - message]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [ - stdClass::class => InvokableFactory::class, - ], - 'delegators' => [ - stdClass::class => [ - TestAsset\PreDelegator::class, - static function ($container, $name, $callback) { - $instance = $callback(); - $instance->foo = 'bar'; - - return $instance; - }, - ], - ], - ]]]> - - - - - - - - - - - - - - + + + + + - - [ - stdClass::class => static function (ServiceLocatorInterface $serviceLocator, $className): stdClass { - self::assertEquals(stdClass::class, $className); - - return new stdClass(); - }, - ], - ]]]> - [ - stdClass::class => static function (ServiceLocatorInterface $serviceLocator, $className): stdClass { - self::assertEquals(stdClass::class, $className); - - return new stdClass(); - }, - ], - ]]]> - - - - - - - - - - @@ -488,84 +145,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - ,non-empty-string} - * >]]> - , string}>]]> - - @@ -584,7 +178,6 @@ - @@ -598,85 +191,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - foo]]> - option]]> - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - diff --git a/psalm.xml.dist b/psalm.xml.dist index 6760f4c2..859a4f6c 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -10,7 +10,6 @@ findUnusedBaselineEntry="true" > - @@ -21,6 +20,7 @@ + @@ -34,9 +34,6 @@ - - - @@ -44,14 +41,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AbstractFactory/ConfigAbstractFactory.php b/src/AbstractFactory/ConfigAbstractFactory.php index 3b58ad8f..4629a6d2 100644 --- a/src/AbstractFactory/ConfigAbstractFactory.php +++ b/src/AbstractFactory/ConfigAbstractFactory.php @@ -22,7 +22,7 @@ final class ConfigAbstractFactory implements AbstractFactoryInterface * * {@inheritdoc} */ - public function canCreate(ContainerInterface $container, $requestedName) + public function canCreate(ContainerInterface $container, string $requestedName): bool { if (! $container->has('config')) { return false; @@ -37,7 +37,7 @@ public function canCreate(ContainerInterface $container, $requestedName) } /** {@inheritDoc} */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { if (! $container->has('config')) { throw new ServiceNotCreatedException('Cannot find a config array in the container'); diff --git a/src/AbstractFactory/ReflectionBasedAbstractFactory.php b/src/AbstractFactory/ReflectionBasedAbstractFactory.php index 4f4654bd..32e23cab 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\ConstructorParameterResolver; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\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,72 +64,44 @@ * * 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 readonly array $aliases = [], + ?ConstructorParameterResolverInterface $constructorParameterResolver = null, + ) { + $this->constructorParameterResolver = $constructorParameterResolver ?? new ConstructorParameterResolver(); } /** * {@inheritDoc} - * - * @return DispatchableInterface */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): object { - $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); } /** {@inheritDoc} */ - public function canCreate(ContainerInterface $container, $requestedName) + public function canCreate(ContainerInterface $container, string $requestedName): bool { return class_exists($requestedName) && $this->canCallConstructor($requestedName); } @@ -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/AbstractFactoryInterface.php b/src/AbstractFactoryInterface.php deleted file mode 100644 index 2a8ff71b..00000000 --- a/src/AbstractFactoryInterface.php +++ /dev/null @@ -1,53 +0,0 @@ - + * @template-implements PluginManagerInterface * @psalm-import-type ServiceManagerConfiguration from ServiceManager - * @psalm-suppress PropertyNotSetInConstructor + * @psalm-import-type FactoryCallable from ServiceManager + * @psalm-import-type DelegatorCallable from ServiceManager + * @psalm-import-type InitializerCallable from ServiceManager + * @psalm-import-type AbstractFactoriesConfiguration from ServiceManager + * @psalm-import-type DelegatorsConfiguration from ServiceManager + * @psalm-import-type FactoriesConfiguration from ServiceManager + * @psalm-import-type InitializersConfiguration from ServiceManager + * @psalm-import-type LazyServicesConfiguration from ServiceManager */ -abstract class AbstractPluginManager extends ServiceManager implements PluginManagerInterface +abstract class AbstractPluginManager implements PluginManagerInterface { /** * Whether or not to auto-add a FQCN as an invokable if it exists. + */ + protected bool $autoAddInvokableClass = true; + + protected bool $sharedByDefault = true; + + /** + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. * - * @var bool + * @var AbstractFactoryInterface[] */ - protected $autoAddInvokableClass = true; + protected array $abstractFactories = []; /** - * An object type that the created instance must be instanced of + * A list of aliases * - * @var null|string - * @psalm-var null|class-string + * Should map one alias to a service name, or another alias (aliases are recursively resolved) + * + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var string[] */ - protected $instanceOf; + protected array $aliases = []; /** - * Sets the provided $parentLocator as the creation context for all - * factories; for $config, {@see \Laminas\ServiceManager\ServiceManager::configure()} - * for details on its accepted structure. + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. * - * @param null|ConfigInterface|ContainerInterface $configInstanceOrParentLocator - * @param array $config - * @psalm-param ServiceManagerConfiguration $config + * @var DelegatorsConfiguration */ - public function __construct($configInstanceOrParentLocator = null, array $config = []) - { - /** @psalm-suppress DocblockTypeContradiction */ - if ( - null !== $configInstanceOrParentLocator - && ! $configInstanceOrParentLocator instanceof ConfigInterface - && ! $configInstanceOrParentLocator instanceof ContainerInterface - ) { - throw new Exception\InvalidArgumentException(sprintf( - '%s expects a ConfigInterface or ContainerInterface instance as the first argument; received %s', - self::class, - is_object($configInstanceOrParentLocator) - ? $configInstanceOrParentLocator::class - : gettype($configInstanceOrParentLocator) - )); - } + protected array $delegators = []; - if ($configInstanceOrParentLocator instanceof ConfigInterface) { - trigger_error(sprintf( - 'Usage of %s as a constructor argument for %s is now deprecated', - ConfigInterface::class, - static::class - ), E_USER_DEPRECATED); - $config = $configInstanceOrParentLocator->toArray(); - } + /** + * A list of factories (either as string name or callable) + * + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var FactoriesConfiguration + */ + protected array $factories = []; - parent::__construct($config); + /** + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var InitializersConfiguration + */ + protected array $initializers = []; - if (! $configInstanceOrParentLocator instanceof ContainerInterface) { - trigger_error(sprintf( - '%s now expects a %s instance representing the parent container; please update your code', - __METHOD__, - ContainerInterface::class - ), E_USER_DEPRECATED); - } + /** + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var LazyServicesConfiguration + */ + protected array $lazyServices = []; - $this->creationContext = $configInstanceOrParentLocator instanceof ContainerInterface - ? $configInstanceOrParentLocator - : $this; - } + /** + * A list of already loaded services (this act as a local cache) + * + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var array + */ + protected array $services = []; /** - * Override configure() to validate service instances. + * Enable/disable shared instances by service name. * - * @param array $config - * @psalm-param ServiceManagerConfiguration $config - * @return self - * @throws InvalidServiceException If an instance passed in the `services` configuration is invalid for the - * plugin manager. + * Example configuration: + * + * 'shared' => [ + * MyService::class => true, // will be shared, even if "sharedByDefault" is false + * MyOtherService::class => false // won't be shared, even if "sharedByDefault" is true + * ] + * + * @deprecated Please pass the plugin manager configuration via {@see AbstractPluginManager::__construct} instead. + * + * @var array + */ + protected array $shared = []; + + private ServiceManager $plugins; + + /** + * @param ServiceManagerConfiguration $config + */ + public function __construct( + ContainerInterface $creationContext, + array $config = [], + ) { + $this->plugins = new ServiceManager([ + 'shared_by_default' => $this->sharedByDefault, + ], $creationContext); + + /** @var ServiceManagerConfiguration $config */ + $config = ArrayUtils::merge([ + 'factories' => $this->factories, + 'abstract_factories' => $this->abstractFactories, + 'aliases' => $this->aliases, + 'services' => $this->services, + 'lazy_services' => $this->lazyServices, + 'shared' => $this->shared, + 'delegators' => $this->delegators, + 'initializers' => $this->initializers, + ], $config); + + $this->configure($config); + } + + /** + * @param ServiceManagerConfiguration $config * @throws ContainerModificationsNotAllowedException If the allow override flag has been toggled off, and a * service instanceexists for a given service. + * @throws InvalidServiceException If an instance passed in the `services` configuration is invalid for the + * plugin manager. + * @throws CyclicAliasException If the configuration contains aliases targeting themselves. */ - public function configure(array $config) + public function configure(array $config): static { if (isset($config['services'])) { foreach ($config['services'] as $service) { @@ -122,99 +161,188 @@ public function configure(array $config) } } - parent::configure($config); + // phpcs:disable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable + /** @var ServiceManagerConfiguration $config */ + $this->plugins->configure($config); + // phpcs:enable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable return $this; } /** - * Override setService for additional plugin validation. - * - * {@inheritDoc} + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. * * @param string|class-string $name * @param InstanceType $service */ - public function setService($name, $service) + public function setService(string $name, mixed $service): void { $this->validate($service); - parent::setService($name, $service); + $this->plugins->setService($name, $service); } /** - * @param class-string|string $name Service name of plugin to retrieve. - * @param null|array $options Options to use when creating the instance. - * @return mixed - * @psalm-return ($name is class-string ? InstanceType : mixed) - * @throws Exception\ServiceNotFoundException If the manager does not have - * a service definition for the instance, and the service is not - * auto-invokable. - * @throws InvalidServiceException If the plugin created is invalid for the - * plugin context. + * {@inheritDoc} */ - public function get($name, ?array $options = null) + public function get(string $id): mixed { - if (! $this->has($name)) { - if (! $this->autoAddInvokableClass || ! class_exists($name)) { + if (! $this->has($id)) { + if (! $this->autoAddInvokableClass || ! class_exists($id)) { throw new Exception\ServiceNotFoundException(sprintf( 'A plugin by the name "%s" was not found in the plugin manager %s', - $name, + $id, static::class )); } - $this->setFactory($name, Factory\InvokableFactory::class); + $this->plugins->setFactory($id, Factory\InvokableFactory::class); } - $instance = ! $options ? parent::get($name) : $this->build($name, $options); + $instance = $this->plugins->get($id); $this->validate($instance); return $instance; } /** * {@inheritDoc} + */ + public function has(string $id): bool + { + return $this->plugins->has($id); + } + + /** + * {@inheritDoc} + */ + public function build(string $name, ?array $options = null): mixed + { + $plugin = $this->plugins->build($name, $options); + $this->validate($plugin); + + return $plugin; + } + + /** + * Add an alias. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. * - * @psalm-assert InstanceType $instance + * @throws ContainerModificationsNotAllowedException If $alias already + * exists as a service and overrides are disallowed. */ - public function validate(mixed $instance) + public function setAlias(string $alias, string $target): void { - if (method_exists($this, 'validatePlugin')) { - trigger_error(sprintf( - '%s::validatePlugin() has been deprecated as of 3.0; please define validate() instead', - static::class - ), E_USER_DEPRECATED); - $this->validatePlugin($instance); - return; - } + $this->plugins->setAlias($alias, $target); + } - if (empty($this->instanceOf) || $instance instanceof $this->instanceOf) { - return; - } + /** + * Add an invokable class mapping. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. + * + * @param null|string $class Class to which to map; if omitted, $name is + * assumed. + * @throws ContainerModificationsNotAllowedException If $name already + * exists as a service and overrides are disallowed. + */ + public function setInvokableClass(string $name, string|null $class = null): void + { + $this->plugins->setInvokableClass($name, $class); + } + + /** + * Specify a factory for a given service name. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. + * + * @phpcs:disable Generic.Files.LineLength.TooLong + * @param class-string|class-string|FactoryCallable|FactoryInterface $factory + * @phpcs:enable Generic.Files.LineLength.TooLong + * @throws ContainerModificationsNotAllowedException If $name already + * exists as a service and overrides are disallowed. + */ + public function setFactory(string $name, string|callable|Factory\FactoryInterface $factory): void + { + $this->plugins->setFactory($name, $factory); + } + + /** + * Create a lazy service mapping to a class. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. + * + * @param string|class-string $name Service name to map + * @param null|class-string $class Class to which to map; if not provided, $name + * will be used for the mapping. + */ + public function mapLazyService(string $name, string|null $class = null): void + { + $this->plugins->mapLazyService($name, $class); + } - throw new InvalidServiceException(sprintf( - 'Plugin manager "%s" expected an instance of type "%s", but "%s" was received', - self::class, - $this->instanceOf, - is_object($instance) ? $instance::class : gettype($instance) - )); + /** + * Add an abstract factory for resolving services. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. + * + * @param string|AbstractFactoryInterface $factory Abstract factory + * instance or class name. + * @psalm-param class-string|AbstractFactoryInterface $factory + */ + public function addAbstractFactory(string|AbstractFactoryInterface $factory): void + { + $this->plugins->addAbstractFactory($factory); } /** - * Implemented for backwards compatibility only. + * Add a delegator for a given service. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. * - * Returns the creation context. + * @param string $name Service name + * @param string|callable|DelegatorFactoryInterface $factory Delegator factory to assign. + * @phpcs:disable Generic.Files.LineLength.TooLong + * @psalm-param class-string|class-string|DelegatorCallable $factory + * @phpcs:enable Generic.Files.LineLength.TooLong + */ + public function addDelegator(string $name, string|callable|DelegatorFactoryInterface $factory): void + { + $this->plugins->addDelegator($name, $factory); + } + + /** + * Add an initializer. * - * @deprecated since 3.0.0. The creation context should be passed during - * instantiation instead. + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. * - * @return void + * @psalm-param class-string|InitializerCallable|InitializerInterface $initializer */ - public function setServiceLocator(ContainerInterface $container) + public function addInitializer(string|callable|InitializerInterface $initializer): void + { + $this->plugins->addInitializer($initializer); + } + + /** + * Add a service sharing rule. + * + * @deprecated Please use {@see AbstractPluginManager::configure()} instead. + * + * @param bool $flag Whether or not the service should be shared. + * @throws ContainerModificationsNotAllowedException If $name already + * exists as a service and overrides are disallowed. + */ + public function setShared(string $name, bool $flag): void + { + $this->plugins->setShared($name, $flag); + } + + public function getAllowOverride(): bool + { + return $this->plugins->getAllowOverride(); + } + + public function setAllowOverride(bool $flag): void { - trigger_error(sprintf( - 'Usage of %s is deprecated since v3.0.0; please pass the container to the constructor instead', - __METHOD__ - ), E_USER_DEPRECATED); - $this->creationContext = $container; + $this->plugins->setAllowOverride($flag); } } diff --git a/src/AbstractSingleInstancePluginManager.php b/src/AbstractSingleInstancePluginManager.php new file mode 100644 index 00000000..e56ce5eb --- /dev/null +++ b/src/AbstractSingleInstancePluginManager.php @@ -0,0 +1,45 @@ + + */ +abstract class AbstractSingleInstancePluginManager extends AbstractPluginManager +{ + /** + * An object type that the created instance must be instanced of + * + * @var class-string + */ + protected string $instanceOf; + + /** + * {@inheritDoc} + */ + public function validate(mixed $instance): void + { + if ($instance instanceof $this->instanceOf) { + return; + } + + throw new InvalidServiceException(sprintf( + 'Plugin manager "%s" expected an instance of type "%s", but "%s" was received', + static::class, + $this->instanceOf, + get_debug_type($instance) + )); + } +} diff --git a/src/Command/AheadOfTimeFactoryCreatorCommand.php b/src/Command/AheadOfTimeFactoryCreatorCommand.php new file mode 100644 index 00000000..9ba1ccb5 --- /dev/null +++ b/src/Command/AheadOfTimeFactoryCreatorCommand.php @@ -0,0 +1,167 @@ +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. In case you are targeting a nonexistent directory, please create the appropriate directory' + . ' structure before executing this command.', + ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH, + )); + + return self::FAILURE; + } + + $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( + '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) + ); + + $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)); + } + } + + $factoryFileName = sprintf( + '%s/%s.php', + $targetDirectory, + str_replace('\\', '_', $factoryClassName) + ); + file_put_contents($factoryFileName, $factory->generatedFactory); + if (! isset($containerConfigurations[$factory->containerConfigurationKey])) { + $containerConfigurations[$factory->containerConfigurationKey] = ['factories' => []]; + } + + // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.NoAssignment + /** + * Psalm has to understand that the `factoryClassName` variable contains a class-string to a factory which + * will be available once persisted to the filesystem and loaded via composer autoloading. + * + * @psalm-suppress UnnecessaryVarAnnotation Sadly, we do have to do this as psalm is not able to infer + * concatenated arrays. + * @var class-string $factoryClassName + */ + $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); + assert($aheadOfTimeFactoryCompiler instanceof AheadOfTimeFactoryCompilerInterface); + $config = $container->has('config') ? $container->get('config') : []; + if (! is_iterable($config)) { + return new AheadOfTimeFactoryCreatorCommand([], '', $aheadOfTimeFactoryCompiler); + } + + if (! is_array($config)) { + $config = iterator_to_array($config); + } + + $factoryTargetPath = $config[ConfigProvider::CONFIGURATION_KEY_FACTORY_TARGET_PATH] ?? ''; + if (! is_string($factoryTargetPath)) { + $factoryTargetPath = ''; + } + + return new AheadOfTimeFactoryCreatorCommand($config, $factoryTargetPath, $aheadOfTimeFactoryCompiler); + } +} diff --git a/src/Command/ConfigDumperCommand.php b/src/Command/ConfigDumperCommand.php new file mode 100644 index 00000000..fb877402 --- /dev/null +++ b/src/Command/ConfigDumperCommand.php @@ -0,0 +1,168 @@ +addOption( + 'ignore-unresolved', + 'i', + InputOption::VALUE_NONE, + 'Ignore classes with unresolved direct dependencies.', + ); + + $this->addArgument( + 'configFile', + InputArgument::REQUIRED, + '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.' + ); + + $this->addArgument( + 'class', + InputArgument::REQUIRED, + 'Name of the class to reflect and for which to generate dependency configuration.' + ); + + $this->setDescription( + '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.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $configFile = $input->getArgument('configFile'); + assert(is_string($configFile)); + + try { + $configFromConfigFile = $this->getConfig($configFile); + } catch (InvalidArgumentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return self::FAILURE; + } + + $class = $input->getArgument('class'); + assert(is_string($class)); + + if (! class_exists($class)) { + $output->writeln(sprintf( + 'Class "%s" does not exist or could not be autoloaded.', + $class + )); + + return self::FAILURE; + } + + try { + $config = $this->configDumper->createDependencyConfig( + $configFromConfigFile, + $class, + $input->hasOption('ignore-unresolved') + ); + } catch (Exception\InvalidArgumentException $exception) { + $output->writeln(sprintf( + 'Unable to create config for "%s": %s', + $class, + $exception->getMessage() + )); + return self::FAILURE; + } + + file_put_contents($configFile, $this->configDumper->dumpConfigFile($config)); + + $output->writeln(sprintf( + '[DONE] Changes written to %s', + $configFile + )); + + return self::SUCCESS; + } + + /** + * @return array + */ + private function getConfig(string $configFile): array + { + if (file_exists($configFile)) { + $config = require $configFile; + + $this->assertConfigurationIsMap($config, $configFile); + + return $config; + } + + if (! is_writable(dirname($configFile))) { + throw new InvalidArgumentException(sprintf( + 'Cannot create configuration at path "%s"; not writable.', + $configFile + )); + } + + return []; + } + + /** + * @psalm-assert array $config + */ + private function assertConfigurationIsMap(mixed $config, string $configFile): void + { + if (! is_array($config)) { + throw new InvalidArgumentException(sprintf( + 'Configuration at path "%s" does not return an array.', + $configFile + )); + } + + if ($config === []) { + return; + } + + foreach (array_keys($config) as $key) { + if (is_string($key)) { + return; + } + } + + throw new InvalidArgumentException(sprintf( + 'Configuration at path "%s" does not return a map of configuration keys.', + $configFile + )); + } +} diff --git a/src/Command/ConfigDumperCommandFactory.php b/src/Command/ConfigDumperCommandFactory.php new file mode 100644 index 00000000..7ad9f48e --- /dev/null +++ b/src/Command/ConfigDumperCommandFactory.php @@ -0,0 +1,23 @@ +get(ConfigDumperInterface::class); + assert($dumper instanceof ConfigDumperInterface); + return new ConfigDumperCommand($dumper); + } +} diff --git a/src/Command/FactoryCreatorCommand.php b/src/Command/FactoryCreatorCommand.php new file mode 100644 index 00000000..fddf0ed6 --- /dev/null +++ b/src/Command/FactoryCreatorCommand.php @@ -0,0 +1,72 @@ +addArgument( + 'className', + InputArgument::REQUIRED, + 'Name of the class to reflect and for which to generate a factory.' + ); + $this->setDescription( + '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.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $className = $input->getArgument('className'); + assert(is_string($className)); + + if (! class_exists($className)) { + $output->writeln(sprintf( + 'Class "%s" does not exist or could not be autoloaded.', + $className + )); + return self::FAILURE; + } + + try { + $factory = $this->factoryCreator->createFactory($className); + } catch (Exception\InvalidArgumentException $e) { + $output->writeln(sprintf( + 'Unable to create factory for "%s": %s', + $className, + $e->getMessage() + )); + return self::FAILURE; + } + + $output->writeln($factory); + return self::SUCCESS; + } +} diff --git a/src/Command/FactoryCreatorCommandFactory.php b/src/Command/FactoryCreatorCommandFactory.php new file mode 100644 index 00000000..3aa63c42 --- /dev/null +++ b/src/Command/FactoryCreatorCommandFactory.php @@ -0,0 +1,23 @@ +get(FactoryCreatorInterface::class); + assert($creator instanceof FactoryCreatorInterface); + return new FactoryCreatorCommand($creator); + } +} diff --git a/src/Config.php b/src/Config.php deleted file mode 100644 index b7b6042c..00000000 --- a/src/Config.php +++ /dev/null @@ -1,102 +0,0 @@ - */ - private array $allowedKeys = [ - 'abstract_factories' => true, - 'aliases' => true, - 'delegators' => true, - 'factories' => true, - 'initializers' => true, - 'invokables' => true, - 'lazy_services' => true, - 'services' => true, - 'shared' => true, - ]; - - /** - * @var array - * @psalm-var ServiceManagerConfigurationType - */ - protected $config = [ - 'abstract_factories' => [], - 'aliases' => [], - 'delegators' => [], - 'factories' => [], - 'initializers' => [], - 'invokables' => [], - 'lazy_services' => [], - 'services' => [], - 'shared' => [], - ]; - - /** - * @psalm-param ServiceManagerConfigurationType $config - */ - public function __construct(array $config = []) - { - // Only merge keys we're interested in - foreach (array_keys($config) as $key) { - if (! isset($this->allowedKeys[$key])) { - unset($config[$key]); - } - } - - /** @psalm-suppress ArgumentTypeCoercion */ - $this->config = $this->merge($this->config, $config); - } - - /** - * @inheritDoc - */ - public function configureServiceManager(ServiceManager $serviceManager) - { - return $serviceManager->configure($this->config); - } - - /** - * @inheritDoc - */ - public function toArray() - { - return $this->config; - } - - /** - * @psalm-param ServiceManagerConfigurationType $a - * @psalm-param ServiceManagerConfigurationType $b - * @psalm-return ServiceManagerConfigurationType - */ - private function merge(array $a, array $b) - { - return ArrayUtils::merge($a, $b); - } -} diff --git a/src/ConfigInterface.php b/src/ConfigInterface.php deleted file mode 100644 index 15fb7e3e..00000000 --- a/src/ConfigInterface.php +++ /dev/null @@ -1,93 +0,0 @@ -|Factory\AbstractFactoryInterface) - * > - * @psalm-type DelegatorsConfigurationType = array< - * string, - * array< - * array-key, - * (class-string|Factory\DelegatorFactoryInterface) - * |callable(ContainerInterface,string,callable():object,array|null):object - * > - * > - * @psalm-type FactoriesConfigurationType = array< - * string, - * (class-string|Factory\FactoryInterface) - * |callable(ContainerInterface,?string,?array|null):object - * > - * @psalm-type InitializersConfigurationType = array< - * array-key, - * (class-string|Initializer\InitializerInterface) - * |callable(ContainerInterface,object):void - * > - * @psalm-type LazyServicesConfigurationType = array{ - * class_map?:array, - * proxies_namespace?:non-empty-string, - * proxies_target_dir?:non-empty-string, - * write_proxy_files?:bool - * } - * @psalm-type ServiceManagerConfigurationType = array{ - * abstract_factories?: AbstractFactoriesConfigurationType, - * aliases?: array, - * delegators?: DelegatorsConfigurationType, - * factories?: FactoriesConfigurationType, - * initializers?: InitializersConfigurationType, - * invokables?: array, - * lazy_services?: LazyServicesConfigurationType, - * services?: array, - * shared?:array, - * ... - * } - */ -interface ConfigInterface -{ - /** - * Configure a service manager. - * - * Implementations should pull configuration from somewhere (typically - * local properties) and pass it to a ServiceManager's withConfig() method, - * returning a new instance. - * - * @return ServiceManager - */ - public function configureServiceManager(ServiceManager $serviceManager); - - /** - * Return configuration for a service manager instance as an array. - * - * Implementations MUST return an array compatible with ServiceManager::configure, - * containing one or more of the following keys: - * - * - abstract_factories - * - aliases - * - delegators - * - factories - * - initializers - * - invokables - * - lazy_services - * - services - * - shared - * - * In other words, this should return configuration that can be used to instantiate - * a service manager or plugin manager, or pass to its `withConfig()` method. - * - * @return array - * @psalm-return ServiceManagerConfigurationType - */ - public function toArray(); -} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php new file mode 100644 index 00000000..655ead54 --- /dev/null +++ b/src/ConfigProvider.php @@ -0,0 +1,86 @@ + + */ + public function __invoke(): array + { + return [ + 'dependencies' => $this->getServiceDependencies(), + 'laminas-cli' => $this->getLaminasCliDependencies(), + ]; + } + + /** + * @return ServiceManagerConfiguration + */ + public function getServiceDependencies(): array + { + $factories = [ + 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 += [ + AheadOfTimeFactoryCreatorCommand::class => AheadOfTimeFactoryCreatorCommandFactory::class, + ConfigDumperCommand::class => ConfigDumperCommandFactory::class, + FactoryCreatorCommand::class => FactoryCreatorCommandFactory::class, + ]; + } + + return [ + 'factories' => $factories, + ]; + } + + /** + * @return array + */ + private function getLaminasCliDependencies(): array + { + if (! class_exists(Command::class)) { + return []; + } + + return [ + 'commands' => [ + ConfigDumperCommand::NAME => ConfigDumperCommand::class, + FactoryCreatorCommand::NAME => FactoryCreatorCommand::class, + AheadOfTimeFactoryCreatorCommand::NAME => AheadOfTimeFactoryCreatorCommand::class, + ], + ]; + } +} diff --git a/src/DelegatorFactoryInterface.php b/src/DelegatorFactoryInterface.php deleted file mode 100644 index 1b30ecf6..00000000 --- a/src/DelegatorFactoryInterface.php +++ /dev/null @@ -1,41 +0,0 @@ - $aliases map of referenced services, indexed by alias name */ public static function fromCyclicAlias(string $alias, array $aliases): self { @@ -37,13 +37,12 @@ public static function fromCyclicAlias(string $alias, array $aliases): self } /** - * @param string[] $aliases map of referenced services, indexed by alias name (string) - * @return self + * @param array $aliases map of referenced services, indexed by alias name (string) */ - public static function fromAliasesMap(array $aliases) + public static function fromAliasesMap(array $aliases): self { $detectedCycles = array_filter(array_map( - static fn($alias): ?array => self::getCycleFor($aliases, $alias), + static fn(string $alias): ?array => self::getCycleFor($aliases, $alias), array_keys($aliases) )); @@ -65,11 +64,10 @@ public static function fromAliasesMap(array $aliases) /** * Retrieves the cycle detected for the given $alias, or `null` if no cycle was detected * - * @param string[] $aliases - * @param string $alias - * @return array|null + * @param array $aliases + * @return array|null */ - private static function getCycleFor(array $aliases, $alias) + private static function getCycleFor(array $aliases, string $alias): ?array { $cycleCandidate = []; $targetName = $alias; @@ -87,10 +85,9 @@ private static function getCycleFor(array $aliases, $alias) } /** - * @param string[] $aliases - * @return string + * @param array $aliases */ - private static function printReferencesMap(array $aliases) + private static function printReferencesMap(array $aliases): string { $map = []; @@ -103,19 +100,17 @@ private static function printReferencesMap(array $aliases) /** * @param string[][] $detectedCycles - * @return string */ - private static function printCycles(array $detectedCycles) + private static function printCycles(array $detectedCycles): string { return "[\n" . implode("\n", array_map([self::class, 'printCycle'], $detectedCycles)) . "\n]"; } /** * @param string[] $detectedCycle - * @return string * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod */ - private static function printCycle(array $detectedCycle) + private static function printCycle(array $detectedCycle): string { $fullCycle = array_keys($detectedCycle); $fullCycle[] = reset($fullCycle); @@ -133,7 +128,7 @@ private static function printCycle(array $detectedCycle) * @param bool[][] $detectedCycles * @return bool[][] de-duplicated */ - private static function deDuplicateDetectedCycles(array $detectedCycles) + private static function deDuplicateDetectedCycles(array $detectedCycles): array { $detectedCyclesByHash = []; diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index dd8ee808..2cca6fd8 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -5,7 +5,6 @@ namespace Laminas\ServiceManager\Exception; use InvalidArgumentException as SplInvalidArgumentException; -use Laminas\ServiceManager\AbstractFactoryInterface; use Laminas\ServiceManager\Initializer\InitializerInterface; use function gettype; @@ -26,14 +25,4 @@ public static function fromInvalidInitializer(mixed $initializer): self is_object($initializer) ? $initializer::class : gettype($initializer) )); } - - public static function fromInvalidAbstractFactory(mixed $abstractFactory): self - { - return new self(sprintf( - 'An invalid abstract factory was registered. Expected an instance of or a valid' - . ' class name resolving to an implementation of "%s", but "%s" was received.', - AbstractFactoryInterface::class, - is_object($abstractFactory) ? $abstractFactory::class : gettype($abstractFactory) - )); - } } 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 @@ + $options - * @return object + * @param callable():mixed $callback * @throws ServiceNotFoundException If unable to resolve the service. * @throws ServiceNotCreatedException If an exception is raised when creating a service. * @throws ContainerExceptionInterface If any other error occurs. */ - public function __invoke(ContainerInterface $container, $name, callable $callback, ?array $options = null); + public function __invoke( + ContainerInterface $container, + string $name, + callable $callback, + ?array $options = null + ): mixed; } diff --git a/src/Factory/FactoryInterface.php b/src/Factory/FactoryInterface.php index 68e61671..1043a211 100644 --- a/src/Factory/FactoryInterface.php +++ b/src/Factory/FactoryInterface.php @@ -12,22 +12,17 @@ /** * Interface for a factory * - * A factory is an callable object that is able to create an object. It is - * given the instance of the service locator, the requested name of the class + * A factory is an callable object that is able to create a service. It is + * given the instance of the service locator, the requested name of the service * you want to create, and any additional options that could be used to - * configure the instance state. + * configure the service state. */ interface FactoryInterface { /** - * Create an object - * - * @param string $requestedName - * @param null|array $options - * @return object * @throws ServiceNotFoundException If unable to resolve the service. * @throws ServiceNotCreatedException If an exception is raised when creating a service. * @throws ContainerExceptionInterface If any other error occurs. */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null); + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed; } diff --git a/src/Factory/InvokableFactory.php b/src/Factory/InvokableFactory.php index 6f80c894..c1b96f08 100644 --- a/src/Factory/InvokableFactory.php +++ b/src/Factory/InvokableFactory.php @@ -20,7 +20,7 @@ final class InvokableFactory implements FactoryInterface { /** {@inheritDoc} */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { return null === $options ? new $requestedName() : new $requestedName($options); } diff --git a/src/FactoryInterface.php b/src/FactoryInterface.php deleted file mode 100644 index f22069ee..00000000 --- a/src/FactoryInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -|string $id Service name of plugin to retrieve. + * @psalm-return ($id is class-string ? TRequestedInstance : InstanceType) + * @throws Exception\ServiceNotFoundException If the manager does not have + * a service definition for the instance, and the service is not + * auto-invokable. + * @throws InvalidServiceException If the plugin created is invalid for the + * plugin context. + */ + public function get(string $id): mixed; + + /** + * Build a service by its name, using optional options (such services are NEVER cached). + * + * @template TRequestedInstance extends InstanceType + * @psalm-param string|class-string $name + * @psalm-return ($name is class-string ? TRequestedInstance : InstanceType) + * @throws Exception\ServiceNotFoundException If no factory/abstract + * factory could be found to create the instance. + * @throws Exception\ServiceNotCreatedException If factory/delegator fails + * to create the instance. + * @throws ContainerExceptionInterface If any other error occurs. + */ + public function build(string $name, ?array $options = null): mixed; } diff --git a/src/Proxy/LazyServiceFactory.php b/src/Proxy/LazyServiceFactory.php index a609c63a..56ccfe4a 100644 --- a/src/Proxy/LazyServiceFactory.php +++ b/src/Proxy/LazyServiceFactory.php @@ -25,18 +25,21 @@ final class LazyServiceFactory implements DelegatorFactoryInterface * @param array $servicesMap A map of service names to * class names of their respective classes */ - public function __construct(private LazyLoadingValueHolderFactory $proxyFactory, private array $servicesMap) - { + public function __construct( + private readonly LazyLoadingValueHolderFactory $proxyFactory, + private readonly array $servicesMap + ) { } /** * {@inheritDoc} - * - * @param string $name - * @return VirtualProxyInterface */ - public function __invoke(ContainerInterface $container, $name, callable $callback, ?array $options = null) - { + public function __invoke( + ContainerInterface $container, + string $name, + callable $callback, + ?array $options = null + ): VirtualProxyInterface { if (isset($this->servicesMap[$name])) { $initializer = static function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($callback): bool { $proxy->setProxyInitializer(null); diff --git a/src/ServiceLocatorInterface.php b/src/ServiceLocatorInterface.php index 3d4bd447..b4e91977 100644 --- a/src/ServiceLocatorInterface.php +++ b/src/ServiceLocatorInterface.php @@ -4,8 +4,11 @@ namespace Laminas\ServiceManager; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; /** * Interface for service locator @@ -13,18 +16,27 @@ interface ServiceLocatorInterface extends ContainerInterface { /** - * Build a service by its name, using optional options (such services are NEVER cached). + * Builds a service by its name, using optional options (such services are NEVER cached). * * @template T of object * @param string|class-string $name - * @param null|array $options - * @return mixed * @psalm-return ($name is class-string ? T : mixed) - * @throws Exception\ServiceNotFoundException If no factory/abstract + * @throws ServiceNotFoundException If no factory/abstract * factory could be found to create the instance. - * @throws Exception\ServiceNotCreatedException If factory/delegator fails + * @throws ServiceNotCreatedException If factory/delegator fails * to create the instance. * @throws ContainerExceptionInterface If any other error occurs. */ - public function build($name, ?array $options = null); + public function build(string $name, ?array $options = null): mixed; + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @template T of object + * @param string|class-string $id + * @psalm-return ($id is class-string ? T : mixed) + * @throws ContainerExceptionInterface Error while retrieving the entry. + * @throws NotFoundExceptionInterface No entry was found for **this** identifier. + */ + public function get(string $id); } diff --git a/src/ServiceManager.php b/src/ServiceManager.php index d63e2631..466702a8 100644 --- a/src/ServiceManager.php +++ b/src/ServiceManager.php @@ -8,8 +8,14 @@ use Laminas\ServiceManager\Exception\ContainerModificationsNotAllowedException; use Laminas\ServiceManager\Exception\CyclicAliasException; use Laminas\ServiceManager\Exception\InvalidArgumentException; +use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Laminas\ServiceManager\Factory\InvokableFactory; +use Laminas\ServiceManager\Initializer\InitializerInterface; use Laminas\ServiceManager\Proxy\LazyServiceFactory; use Laminas\Stdlib\ArrayUtils; use ProxyManager\Configuration as ProxyConfiguration; @@ -23,18 +29,14 @@ use function array_intersect; use function array_key_exists; use function array_keys; +use function array_merge; use function class_exists; -use function gettype; use function in_array; use function is_callable; -use function is_object; use function is_string; use function spl_autoload_register; use function spl_object_hash; use function sprintf; -use function trigger_error; - -use const E_USER_DEPRECATED; /** * Service Manager. @@ -51,29 +53,35 @@ * It also provides the ability to inject specific service instances and to * define aliases. * - * @see ConfigInterface + * @see ContainerInterface + * @see DelegatorFactoryInterface + * @see AbstractFactoryInterface + * @see FactoryInterface * * @psalm-type AbstractFactoriesConfiguration = array< * array-key, - * (class-string|Factory\AbstractFactoryInterface) + * class-string|AbstractFactoryInterface * > + * @psalm-type DelegatorCallable = callable(ContainerInterface,string,callable():mixed,array|null):mixed * @psalm-type DelegatorsConfiguration = array< * string, * array< * array-key, - * (class-string|Factory\DelegatorFactoryInterface) - * |callable(ContainerInterface,string,callable():object,array|null):object + * class-string + * |class-string + * |DelegatorFactoryInterface + * |DelegatorCallable * > * > + * @psalm-type FactoryCallable = callable(ContainerInterface,string,array|null):mixed * @psalm-type FactoriesConfiguration = array< * string, - * (class-string|Factory\FactoryInterface) - * |callable(ContainerInterface,?string,?array|null):object + * class-string|class-string|FactoryInterface|FactoryCallable * > + * @psalm-type InitializerCallable = callable(ContainerInterface,mixed):void * @psalm-type InitializersConfiguration = array< * array-key, - * (class-string|Initializer\InitializerInterface) - * |callable(ContainerInterface,object):void + * class-string|class-string|InitializerInterface|InitializerCallable * > * @psalm-type LazyServicesConfiguration = array{ * class_map?:array, @@ -87,72 +95,61 @@ * delegators?: DelegatorsConfiguration, * factories?: FactoriesConfiguration, * initializers?: InitializersConfiguration, - * invokables?: array, + * invokables?: array, * lazy_services?: LazyServicesConfiguration, - * services?: array>, + * services?: array, * shared?:array, - * shared_by_default?:bool, + * shared_by_default?: bool, * ... * } + * + * @final Will be marked as final with v5.0.0 */ class ServiceManager implements ServiceLocatorInterface { - /** @var Factory\AbstractFactoryInterface[] */ - protected $abstractFactories = []; + /** @var AbstractFactoryInterface[] */ + protected array $abstractFactories = []; /** * A list of aliases * * Should map one alias to a service name, or another alias (aliases are recursively resolved) * - * @var string[] + * @var array */ - protected $aliases = []; + protected array $aliases = []; /** * Whether or not changes may be made to this instance. - * - * @var bool */ - protected $allowOverride = false; + protected bool $allowOverride = false; - /** @var ContainerInterface */ - protected $creationContext; + protected ContainerInterface $creationContext; - /** - * @var string[][]|Factory\DelegatorFactoryInterface[][] - * @psalm-var DelegatorsConfiguration - */ - protected $delegators = []; + /** @var DelegatorsConfiguration */ + protected array $delegators = []; /** * A list of factories (either as string name or callable) * - * @var string[]|callable[] - * @psalm-var FactoriesConfiguration + * @var FactoriesConfiguration */ - protected $factories = []; + protected array $factories = []; - /** - * @var Initializer\InitializerInterface[]|callable[] - * @psalm-var InitializersConfiguration - */ - protected $initializers = []; + /** @var list */ + protected array $initializers = []; - /** - * @var array - * @psalm-var LazyServicesConfiguration - */ - protected $lazyServices = []; + /** @var LazyServicesConfiguration */ + protected array $lazyServices = []; private ?LazyServiceFactory $lazyServicesDelegator = null; /** * A list of already loaded services (this act as a local cache) * - * @var array + * @var array */ - protected $services = []; + protected array $services = []; /** * Enable/disable shared instances by service name. @@ -166,24 +163,22 @@ class ServiceManager implements ServiceLocatorInterface * * @var array */ - protected $shared = []; + protected array $shared = []; /** * Should the services be shared by default? - * - * @var bool */ - protected $sharedByDefault = true; + protected bool $sharedByDefault = true; /** * Service manager was already configured? - * - * @var bool */ - protected $configured = false; + protected bool $configured = false; /** * Cached abstract factories from string. + * + * @var array,AbstractFactoryInterface> */ private array $cachedAbstractFactories = []; @@ -191,62 +186,48 @@ class ServiceManager implements ServiceLocatorInterface * See {@see \Laminas\ServiceManager\ServiceManager::configure()} for details * on what $config accepts. * - * @psalm-param ServiceManagerConfiguration $config + * @param ServiceManagerConfiguration $config */ - public function __construct(array $config = []) - { - $this->creationContext = $this; + public function __construct( + array $config = [], + ContainerInterface|null $creationContext = null + ) { + $this->creationContext = $creationContext ?? $this; $this->configure($config); } - /** - * Implemented for backwards compatibility with previous plugin managers only. - * - * Returns the creation context. - * - * @deprecated since 3.0.0. Factories using 3.0 should use the container - * instance passed to the factory instead. - * - * @return ContainerInterface - */ - public function getServiceLocator() - { - trigger_error(sprintf( - 'Usage of %s is deprecated since v3.0.0; please use the container passed to the factory instead', - __METHOD__ - ), E_USER_DEPRECATED); - return $this->creationContext; - } - /** {@inheritDoc} */ - public function get($name) + public function get(string $id): mixed { // We start by checking if we have cached the requested service; // this is the fastest method. - if (isset($this->services[$name])) { - return $this->services[$name]; + if (isset($this->services[$id])) { + /** @psalm-suppress MixedReturnStatement Yes indeed, service managers can return mixed. */ + return $this->services[$id]; } // Determine if the service should be shared. - $sharedService = $this->shared[$name] ?? $this->sharedByDefault; + $sharedService = $this->shared[$id] ?? $this->sharedByDefault; // We achieve better performance if we can let all alias // considerations out. if (! $this->aliases) { - $object = $this->doCreate($name); + $service = $this->doCreate($id); - // Cache the object for later, if it is supposed to be shared. + // Cache the service for later, if it is supposed to be shared. if ($sharedService) { - $this->services[$name] = $object; + $this->services[$id] = $service; } - return $object; + + /** @psalm-suppress MixedReturnStatement Yes indeed, service managers can return mixed. */ + return $service; } // We now deal with requests which may be aliases. - $resolvedName = $this->aliases[$name] ?? $name; + $resolvedName = $this->aliases[$id] ?? $id; // Update shared service information as we checked if the alias was shared before. - if ($resolvedName !== $name) { + if ($resolvedName !== $id) { $sharedService = $this->shared[$resolvedName] ?? $sharedService; } @@ -255,76 +236,72 @@ public function get($name) // If the alias is configured as a shared service, we are done. if ($sharedAlias) { - $this->services[$name] = $this->services[$resolvedName]; + $this->services[$id] = $this->services[$resolvedName]; + + /** @psalm-suppress MixedReturnStatement Yes indeed, service managers can return mixed. */ return $this->services[$resolvedName]; } // At this point, we have to create the object. // We use the resolved name for that. - $object = $this->doCreate($resolvedName); + $service = $this->doCreate($resolvedName); // Cache the object for later, if it is supposed to be shared. if ($sharedService) { - $this->services[$resolvedName] = $object; - } - - // Also cache under the alias name; this allows sharing based on the - // service name used. - if ($sharedAlias) { - $this->services[$name] = $object; + $this->services[$resolvedName] = $service; + $this->services[$id] = $service; } - return $object; + /** @psalm-suppress MixedReturnStatement Yes indeed, service managers can return mixed. */ + return $service; } /** {@inheritDoc} */ - public function build($name, ?array $options = null) + public function build(string $name, ?array $options = null): mixed { // We never cache when using "build". $name = $this->aliases[$name] ?? $name; + /** @psalm-suppress MixedReturnStatement Yes indeed, service managers can return mixed. */ return $this->doCreate($name, $options); } /** * {@inheritDoc} * - * @param string|class-string $name + * @param string|class-string $id * @return bool */ - public function has($name) + public function has(string $id): bool { // Check static services and factories first to speedup the most common requests. - return $this->staticServiceOrFactoryCanCreate($name) || $this->abstractFactoryCanCreate($name); + return $this->staticServiceOrFactoryCanCreate($id) || $this->abstractFactoryCanCreate($id); } /** * Indicate whether or not the instance is immutable. - * - * @param bool $flag */ - public function setAllowOverride($flag) + public function setAllowOverride(bool $flag): void { - $this->allowOverride = (bool) $flag; + $this->allowOverride = $flag; } /** * Retrieve the flag indicating immutability status. - * - * @return bool */ - public function getAllowOverride() + public function getAllowOverride(): bool { return $this->allowOverride; } /** - * @psalm-param ServiceManagerConfiguration $config - * @return self - * @throws ContainerModificationsNotAllowedException If the allow - * override flag has been toggled off, and a service instance - * exists for a given service. + * @param ServiceManagerConfiguration $config + * @throws ContainerModificationsNotAllowedException If the allow override flag has been toggled off, and a + * service instanceexists for a given service. + * @throws InvalidServiceException If an instance passed in the `services` configuration is invalid for the + * plugin manager. + * @throws CyclicAliasException If the configuration contains aliases targeting themselves. */ - public function configure(array $config) + public function configure(array $config): static { // This is a bulk update/initial configuration, // so we check all definitions up front. @@ -334,7 +311,7 @@ public function configure(array $config) $this->services = $config['services'] + $this->services; } - if (isset($config['invokables']) && ! empty($config['invokables'])) { + if (isset($config['invokables']) && $config['invokables'] !== []) { $newAliases = $this->createAliasesAndFactoriesForInvokables($config['invokables']); // override existing aliases with those created by invokables to ensure // that they are still present after merging aliases later on @@ -353,10 +330,10 @@ public function configure(array $config) $this->shared = $config['shared'] + $this->shared; } - if (! empty($config['aliases'])) { + if (isset($config['aliases']) && $config['aliases'] !== []) { $this->aliases = $config['aliases'] + $this->aliases; $this->mapAliasesToTargets(); - } elseif (! $this->configured && ! empty($this->aliases)) { + } elseif (! $this->configured && $this->aliases !== []) { $this->mapAliasesToTargets(); } @@ -366,7 +343,7 @@ public function configure(array $config) // If lazy service configuration was provided, reset the lazy services // delegator factory. - if (isset($config['lazy_services']) && ! empty($config['lazy_services'])) { + if (isset($config['lazy_services']) && $config['lazy_services'] !== []) { /** @psalm-suppress MixedPropertyTypeCoercion */ $this->lazyServices = ArrayUtils::merge($this->lazyServices, $config['lazy_services']); $this->lazyServicesDelegator = null; @@ -377,7 +354,7 @@ public function configure(array $config) if (isset($config['abstract_factories'])) { $abstractFactories = $config['abstract_factories']; // $key not needed, but foreach is faster than foreach + array_values. - foreach ($abstractFactories as $key => $abstractFactory) { + foreach ($abstractFactories as $abstractFactory) { $this->resolveAbstractFactoryInstance($abstractFactory); } } @@ -394,12 +371,10 @@ public function configure(array $config) /** * Add an alias. * - * @param string $alias - * @param string $target * @throws ContainerModificationsNotAllowedException If $alias already * exists as a service and overrides are disallowed. */ - public function setAlias($alias, $target) + public function setAlias(string $alias, string $target): void { if (isset($this->services[$alias]) && ! $this->allowOverride) { throw ContainerModificationsNotAllowedException::fromExistingService($alias); @@ -417,7 +392,7 @@ public function setAlias($alias, $target) * @throws ContainerModificationsNotAllowedException If $name already * exists as a service and overrides are disallowed. */ - public function setInvokableClass($name, $class = null) + public function setInvokableClass(string $name, ?string $class = null): void { if (isset($this->services[$name]) && ! $this->allowOverride) { throw ContainerModificationsNotAllowedException::fromExistingService($name); @@ -430,15 +405,14 @@ public function setInvokableClass($name, $class = null) * Specify a factory for a given service name. * * @param string $name Service name - * @param string|callable|Factory\FactoryInterface $factory Factory to which to map. - * phpcs:disable Generic.Files.LineLength.TooLong - * @psalm-param class-string|callable(ContainerInterface,string,array|null):object|Factory\FactoryInterface $factory - * phpcs:enable Generic.Files.LineLength.TooLong - * @return void + * @param string|callable|FactoryInterface $factory Factory to which to map. + * @phpcs:disable Generic.Files.LineLength.TooLong + * @psalm-param class-string|class-string|FactoryCallable|FactoryInterface $factory + * @phpcs:enable Generic.Files.LineLength.TooLong * @throws ContainerModificationsNotAllowedException If $name already * exists as a service and overrides are disallowed. */ - public function setFactory($name, $factory) + public function setFactory(string $name, string|callable|FactoryInterface $factory): void { if (isset($this->services[$name]) && ! $this->allowOverride) { throw ContainerModificationsNotAllowedException::fromExistingService($name); @@ -450,23 +424,37 @@ public function setFactory($name, $factory) /** * Create a lazy service mapping to a class. * - * @param string $name Service name to map - * @param null|string $class Class to which to map; if not provided, $name + * @param string|class-string $name Service name to map + * @param null|class-string $class Class to which to map; if not provided, $name * will be used for the mapping. */ - public function mapLazyService($name, $class = null) + public function mapLazyService(string $name, ?string $class = null): void { - $this->configure(['lazy_services' => ['class_map' => [$name => $class ?: $name]]]); + $targetClassName = $class ?? $name; + if (! class_exists($targetClassName)) { + $message = sprintf('Provided service name "%s" must be a `class-string`.', $name); + if ($class !== null) { + $message = sprintf( + 'Provided service name "%s" must target to a `class-string`. "%s" provided.', + $name, + $class, + ); + } + + throw new InvalidArgumentException($message); + } + + $this->configure(['lazy_services' => ['class_map' => [$name => $targetClassName]]]); } /** * Add an abstract factory for resolving services. * - * @param string|Factory\AbstractFactoryInterface $factory Abstract factory + * @param string|AbstractFactoryInterface $factory Abstract factory * instance or class name. - * @psalm-param class-string|Factory\AbstractFactoryInterface $factory + * @psalm-param class-string|AbstractFactoryInterface $factory */ - public function addAbstractFactory($factory) + public function addAbstractFactory(string|AbstractFactoryInterface $factory): void { $this->resolveAbstractFactoryInstance($factory); } @@ -475,12 +463,13 @@ public function addAbstractFactory($factory) * Add a delegator for a given service. * * @param string $name Service name - * @param string|callable|Factory\DelegatorFactoryInterface $factory Delegator + * @param string|callable|DelegatorFactoryInterface $factory Delegator * factory to assign. - * @psalm-param class-string - * |callable(ContainerInterface,string,callable,array|null) $factory + * @phpcs:disable Generic.Files.LineLength.TooLong + * @psalm-param class-string|class-string|DelegatorCallable|DelegatorFactoryInterface $factory + * @phpcs:enable Generic.Files.LineLength.TooLong */ - public function addDelegator($name, $factory) + public function addDelegator(string $name, string|callable|DelegatorFactoryInterface $factory): void { $this->configure(['delegators' => [$name => [$factory]]]); } @@ -488,12 +477,11 @@ public function addDelegator($name, $factory) /** * Add an initializer. * - * @param string|callable|Initializer\InitializerInterface $initializer - * @psalm-param class-string - * |callable(ContainerInterface,mixed):void - * |Initializer\InitializerInterface $initializer + * @phpcs:disable Generic.Files.LineLength.TooLong + * @psalm-param class-string|class-string|InitializerCallable|InitializerInterface $initializer + * @phpcs:enable Generic.Files.LineLength.TooLong */ - public function addInitializer($initializer) + public function addInitializer(string|callable|InitializerInterface $initializer): void { $this->configure(['initializers' => [$initializer]]); } @@ -502,11 +490,10 @@ public function addInitializer($initializer) * Map a service. * * @param string $name Service name - * @param array|object $service * @throws ContainerModificationsNotAllowedException If $name already * exists as a service and overrides are disallowed. */ - public function setService($name, $service) + public function setService(string $name, mixed $service): void { if (isset($this->services[$name]) && ! $this->allowOverride) { throw ContainerModificationsNotAllowedException::fromExistingService($name); @@ -522,53 +509,64 @@ public function setService($name, $service) * @throws ContainerModificationsNotAllowedException If $name already * exists as a service and overrides are disallowed. */ - public function setShared($name, $flag) + public function setShared(string $name, bool $flag): void { if (isset($this->services[$name]) && ! $this->allowOverride) { throw ContainerModificationsNotAllowedException::fromExistingService($name); } - $this->shared[$name] = (bool) $flag; + $this->shared[$name] = $flag; } /** * Instantiate initializers for to avoid checks during service construction. * - * @psalm-param InitializersConfiguration $initializers + * @param InitializersConfiguration $initializers */ private function resolveInitializers(array $initializers): void { + $resolved = []; foreach ($initializers as $initializer) { if (is_string($initializer) && class_exists($initializer)) { + /** + * @psalm-suppress MixedMethodCall We are calling an unknown initializer. + * We have to trust that the class is instantiable. + */ $initializer = new $initializer(); } if (is_callable($initializer)) { - $this->initializers[] = $initializer; + /** @psalm-var InitializerCallable $initializer */ + $resolved[] = $initializer; continue; } throw InvalidArgumentException::fromInvalidInitializer($initializer); } + + $this->initializers = array_merge($this->initializers, $resolved); } /** * Get a factory for the given service name * - * @psalm-return (callable(ContainerInterface,string,array|null):object)|Factory\FactoryInterface - * @throws ServiceNotFoundException + * @return FactoryCallable|FactoryInterface + * @throws ServiceNotFoundException In case that the service creation strategy based on factories + * did not find any capable factory. */ - private function getFactory(string $name): callable + private function getFactory(string $name): callable|FactoryInterface { $factory = $this->factories[$name] ?? null; $lazyLoaded = false; if (is_string($factory) && class_exists($factory)) { + /** @psalm-suppress MixedMethodCall We have to trust that the factory is instantiable. */ $factory = new $factory(); $lazyLoaded = true; } if (is_callable($factory)) { + /** @psalm-var FactoryCallable $factory */ if ($lazyLoaded) { $this->factories[$name] = $factory; } @@ -589,10 +587,7 @@ private function getFactory(string $name): callable )); } - /** - * @return object - */ - private function createDelegatorFromName(string $name, ?array $options = null) + private function createDelegatorFromName(string $name, ?array $options = null): mixed { $creationCallback = function () use ($name, $options) { // Code is inlined for performance reason, instead of abstracting the creation @@ -602,24 +597,17 @@ private function createDelegatorFromName(string $name, ?array $options = null) $initialCreationContext = $this->creationContext; + $resolvedDelegators = []; foreach ($this->delegators[$name] as $index => $delegatorFactory) { - $delegatorFactory = $this->delegators[$name][$index]; - - if ($delegatorFactory === LazyServiceFactory::class) { - $delegatorFactory = $this->createLazyServiceDelegatorFactory(); - } elseif (is_string($delegatorFactory) && class_exists($delegatorFactory)) { - $delegatorFactory = new $delegatorFactory(); - } - - $this->assertCallableDelegatorFactory($delegatorFactory); - + /** @psalm-suppress ArgumentTypeCoercion https://github.com/vimeo/psalm/issues/9680 */ + $delegatorFactory = $this->resolveDelegatorFactory($delegatorFactory); $this->delegators[$name][$index] = $delegatorFactory; - - $creationCallback = - /** @return object */ - static fn() => $delegatorFactory($initialCreationContext, $name, $creationCallback, $options); + $creationCallback = + static fn(): mixed => $delegatorFactory($initialCreationContext, $name, $creationCallback, $options); } + $this->delegators[$name] = $resolvedDelegators; + return $creationCallback(); } @@ -628,20 +616,19 @@ private function createDelegatorFromName(string $name, ?array $options = null) * * This is a highly performance sensitive method, do not modify if you have not benchmarked it carefully * - * @return object * @throws ServiceNotFoundException If unable to resolve the service. * @throws ServiceNotCreatedException If an exception is raised when creating a service. * @throws ContainerExceptionInterface If any other error occurs. */ - private function doCreate(string $resolvedName, ?array $options = null) + private function doCreate(string $resolvedName, ?array $options = null): mixed { try { if (! isset($this->delegators[$resolvedName])) { // Let's create the service by fetching the factory $factory = $this->getFactory($resolvedName); - $object = $factory($this->creationContext, $resolvedName, $options); + $service = $factory($this->creationContext, $resolvedName, $options); } else { - $object = $this->createDelegatorFromName($resolvedName, $options); + $service = $this->createDelegatorFromName($resolvedName, $options); } } catch (ContainerExceptionInterface $exception) { throw $exception; @@ -654,10 +641,10 @@ private function doCreate(string $resolvedName, ?array $options = null) } foreach ($this->initializers as $initializer) { - $initializer($this->creationContext, $object); + $initializer($this->creationContext, $service); } - return $object; + return $service; } /** @@ -711,8 +698,8 @@ private function createLazyServiceDelegatorFactory(): LazyServiceFactory * It works with strings and class instances. * It's not possible to de-duple anonymous functions * - * @psalm-param DelegatorsConfiguration $config - * @psalm-return DelegatorsConfiguration + * @param DelegatorsConfiguration $config + * @return DelegatorsConfiguration */ private function mergeDelegators(array $config): array { @@ -747,7 +734,7 @@ private function createAliasesAndFactoriesForInvokables(array $invokables): arra $newAliases = []; foreach ($invokables as $name => $class) { - $this->factories[$class] = Factory\InvokableFactory::class; + $this->factories[$class] = InvokableFactory::class; if ($name !== $class) { $this->aliases[$name] = $class; $newAliases[$name] = $class; @@ -767,7 +754,7 @@ private function createAliasesAndFactoriesForInvokables(array $invokables): arra * a given service name we do not have a service instance * in the cache OR override is explicitly allowed. * - * @psalm-param ServiceManagerConfiguration $config + * @param ServiceManagerConfiguration $config * @throws ContainerModificationsNotAllowedException If any * service key is invalid. */ @@ -922,12 +909,11 @@ private function mapAliasesToTargets(): void /** * Instantiate abstract factories in order to avoid checks during service construction. * - * @param string|Factory\AbstractFactoryInterface $abstractFactory - * @psalm-param class-string|Factory\AbstractFactoryInterface $abstractFactory + * @param class-string|AbstractFactoryInterface $abstractFactory */ - private function resolveAbstractFactoryInstance($abstractFactory): void + private function resolveAbstractFactoryInstance(string|AbstractFactoryInterface $abstractFactory): void { - if (is_string($abstractFactory) && class_exists($abstractFactory)) { + if (is_string($abstractFactory)) { // Cached string factory name if (! isset($this->cachedAbstractFactories[$abstractFactory])) { $this->cachedAbstractFactories[$abstractFactory] = new $abstractFactory(); @@ -936,10 +922,6 @@ private function resolveAbstractFactoryInstance($abstractFactory): void $abstractFactory = $this->cachedAbstractFactories[$abstractFactory]; } - if (! $abstractFactory instanceof Factory\AbstractFactoryInterface) { - throw InvalidArgumentException::fromInvalidAbstractFactory($abstractFactory); - } - $abstractFactoryObjHash = spl_object_hash($abstractFactory); $this->abstractFactories[$abstractFactoryObjHash] = $abstractFactory; } @@ -981,30 +963,23 @@ private function abstractFactoryCanCreate(string $name): bool } /** - * @psalm-param mixed $delegatorFactory - * @psalm-assert callable(ContainerInterface,string,callable():object,array|null):object $delegatorFactory + * @param class-string|DelegatorCallable|DelegatorFactoryInterface $delegatorFactory + * @return DelegatorCallable */ - private function assertCallableDelegatorFactory($delegatorFactory): void + private function resolveDelegatorFactory(DelegatorFactoryInterface|string|callable $delegatorFactory): callable { - if ( - $delegatorFactory instanceof Factory\DelegatorFactoryInterface - || is_callable($delegatorFactory) - ) { - return; + if ($delegatorFactory === LazyServiceFactory::class) { + return $this->createLazyServiceDelegatorFactory(); } + + if (is_callable($delegatorFactory)) { + return $delegatorFactory; + } + if (is_string($delegatorFactory)) { - throw new ServiceNotCreatedException(sprintf( - 'An invalid delegator factory was registered; resolved to class or function "%s"' - . ' which does not exist; please provide a valid function name or class name resolving' - . ' to an implementation of %s', - $delegatorFactory, - DelegatorFactoryInterface::class - )); + $delegatorFactory = new $delegatorFactory(); } - throw new ServiceNotCreatedException(sprintf( - 'A non-callable delegator, "%s", was provided; expected a callable or instance of "%s"', - is_object($delegatorFactory) ? $delegatorFactory::class : gettype($delegatorFactory), - DelegatorFactoryInterface::class - )); + + return $delegatorFactory; } } diff --git a/src/Test/CommonPluginManagerTrait.php b/src/Test/CommonPluginManagerTrait.php index 7534de4a..3ae722ce 100644 --- a/src/Test/CommonPluginManagerTrait.php +++ b/src/Test/CommonPluginManagerTrait.php @@ -5,60 +5,41 @@ namespace Laminas\ServiceManager\Test; use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\AbstractSingleInstancePluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; -use ReflectionClass; +use Laminas\ServiceManager\ServiceManager; use ReflectionProperty; use stdClass; -use function method_exists; +use function assert; +use function is_string; /** - * Trait for testing plugin managers for v2-v3 compatibility + * Trait for testing plugin managers for compatibility * * To use this trait: * * implement the `getPluginManager()` method to return your plugin manager - * * implement the `getV2InvalidPluginException()` method to return the class `validatePlugin()` throws under v2 + * + * @psalm-import-type ServiceManagerConfiguration from ServiceManager */ trait CommonPluginManagerTrait { - public function testInstanceOfMatches() + public function testInstanceOfMatches(): void { - $manager = $this->getPluginManager(); + $manager = self::getPluginManager(); $reflection = new ReflectionProperty($manager, 'instanceOf'); $this->assertEquals($this->getInstanceOf(), $reflection->getValue($manager), 'instanceOf does not match'); } - public function testShareByDefaultAndSharedByDefault() - { - $manager = $this->getPluginManager(); - $reflection = new ReflectionClass($manager); - $shareByDefault = $sharedByDefault = true; - - foreach ($reflection->getProperties() as $prop) { - if ($prop->getName() === 'shareByDefault') { - $shareByDefault = $prop->getValue($manager); - } - if ($prop->getName() === 'sharedByDefault') { - $sharedByDefault = $prop->getValue($manager); - } - } - - $this->assertSame( - $shareByDefault, - $sharedByDefault, - 'Values of shareByDefault and sharedByDefault do not match' - ); - } - - public function testRegisteringInvalidElementRaisesException() + public function testRegisteringInvalidElementRaisesException(): void { $this->expectException($this->getServiceNotFoundException()); - $this->getPluginManager()->setService('test', $this); + self::getPluginManager()->setService('test', $this); } - public function testLoadingInvalidElementRaisesException() + public function testLoadingInvalidElementRaisesException(): void { - $manager = $this->getPluginManager(); + $manager = self::getPluginManager(); $manager->setInvokableClass('test', stdClass::class); $this->expectException($this->getServiceNotFoundException()); $manager->get('test'); @@ -66,55 +47,48 @@ public function testLoadingInvalidElementRaisesException() /** * @dataProvider aliasProvider - * @param string $alias - * @param string $expected */ - public function testPluginAliasesResolve($alias, $expected) + public function testPluginAliasesResolve(string $alias, string $expected): void { - $this->assertInstanceOf($expected, $this->getPluginManager()->get($alias), "Alias '$alias' does not resolve'"); + $this->assertInstanceOf($expected, self::getPluginManager()->get($alias), "Alias '$alias' does not resolve'"); } /** - * @return array + * @return list */ public static function aliasProvider(): array { - $manager = self::getPluginManager(); - $reflection = new ReflectionProperty($manager, 'aliases'); + $manager = self::getPluginManager(); + $pluginContainerProperty = new ReflectionProperty(AbstractPluginManager::class, 'plugins'); + $pluginContainer = $pluginContainerProperty->getValue($manager); + self::assertInstanceOf(ServiceManager::class, $pluginContainer); + + $reflection = new ReflectionProperty($pluginContainer, 'aliases'); $data = []; - foreach ($reflection->getValue($manager) as $alias => $expected) { + foreach ($reflection->getValue($pluginContainer) as $alias => $expected) { + assert(is_string($alias) && is_string($expected)); $data[] = [$alias, $expected]; } + return $data; } protected function getServiceNotFoundException(): string { - $manager = $this->getPluginManager(); - if (method_exists($manager, 'configure')) { - return InvalidServiceException::class; - } - return $this->getV2InvalidPluginException(); + return InvalidServiceException::class; } /** * Returns the plugin manager to test * - * @return AbstractPluginManager - */ - abstract protected static function getPluginManager(); - - /** - * Returns the FQCN of the exception thrown under v2 by `validatePlugin()` - * - * @return mixed + * @param ServiceManagerConfiguration $config */ - abstract protected function getV2InvalidPluginException(); + abstract protected static function getPluginManager(array $config = []): AbstractSingleInstancePluginManager; /** * Returns the value the instanceOf property has been set to * - * @return string + * @return class-string */ - abstract protected function getInstanceOf(); + abstract protected function getInstanceOf(): string; } diff --git a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php new file mode 100644 index 00000000..f7394b3b --- /dev/null +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeCompiledFactory.php @@ -0,0 +1,22 @@ +extractServicesRegisteredByReflectionBasedFactory( + $config + ); + + $compiledFactories = []; + + foreach ($servicesRegisteredByReflectionBasedFactory as $service => [$containerConfigurationKey, $aliases]) { + $compiledFactories[] = new AheadOfTimeCompiledFactory( + $service, + $containerConfigurationKey, + $this->factoryCreator->createFactory($service, $aliases), + ); + } + + return $compiledFactories; + } + + /** + * @return array}> + */ + private function extractServicesRegisteredByReflectionBasedFactory(array $config): array + { + $services = []; + + foreach ($config as $key => $entry) { + if (! is_string($key) || $key === '' || ! 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; + } + + foreach ($servicesUsingReflectionBasedFactory as $service => $factory) { + 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.', + $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; + } + + /** + * 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); + } +} diff --git a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php new file mode 100644 index 00000000..c531338a --- /dev/null +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerFactory.php @@ -0,0 +1,20 @@ +get(FactoryCreatorInterface::class); + assert($creator instanceof FactoryCreatorInterface); + return new AheadOfTimeFactoryCompiler($creator); + } +} diff --git a/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php new file mode 100644 index 00000000..c954a22f --- /dev/null +++ b/src/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerInterface.php @@ -0,0 +1,13 @@ + + */ + public function compile(array $config): array; +} diff --git a/src/Tool/ConfigDumper.php b/src/Tool/ConfigDumper.php index 6dd6b7f6..87e6c428 100644 --- a/src/Tool/ConfigDumper.php +++ b/src/Tool/ConfigDumper.php @@ -10,25 +10,28 @@ use ReflectionClass; use ReflectionNamedType; use ReflectionParameter; -use Traversable; use function array_filter; use function array_key_exists; +use function array_keys; use function class_exists; use function date; use function gettype; use function implode; -use function interface_exists; use function is_array; use function is_int; +use function is_iterable; use function is_string; use function sprintf; use function str_repeat; use function var_export; -class ConfigDumper +/** + * @internal + */ +final class ConfigDumper implements ConfigDumperInterface { - public const CONFIG_TEMPLATE = <<validateClassName($className); - $reflectionClass = new ReflectionClass($className); + $constructor = $reflectionClass->getConstructor(); - // class is an interface; do nothing - if ($reflectionClass->isInterface()) { - return $config; + // class has no constructor, treat it as an invokable + if ($constructor === null) { + return $this->createInvokable($config, $className); } - // class has no constructor, treat it as an invokable - if (! $reflectionClass->getConstructor()) { + // has no required parameters, treat it as an invokable + if ($constructor->getNumberOfRequiredParameters() === 0) { return $this->createInvokable($config, $className); } - $constructorArguments = $reflectionClass->getConstructor()->getParameters(); $constructorArguments = array_filter( - $constructorArguments, + $constructor->getParameters(), static fn(ReflectionParameter $argument): bool => ! $argument->isOptional() ); - // has no required parameters, treat it as an invokable - if (empty($constructorArguments)) { - return $this->createInvokable($config, $className); - } - $classConfig = []; foreach ($constructorArguments as $constructorArgument) { @@ -87,18 +81,25 @@ public function createDependencyConfig(array $config, $className, $ignoreUnresol // 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; } + throw new InvalidArgumentException(sprintf( 'Cannot create config for constructor argument "%s", ' . 'it has no type hint, or non-class/interface type hint', $constructorArgument->getName() )); } - $config = $this->createDependencyConfig($config, $argumentType, $ignoreUnresolved); + $classConfig[] = $argumentType; + if (! class_exists($argumentType)) { + continue; + } + + $config = $this->createDependencyConfig($config, $argumentType, $ignoreUnresolved); } $config[ConfigAbstractFactory::class][$className] = $classConfig; @@ -107,37 +108,17 @@ public function createDependencyConfig(array $config, $className, $ignoreUnresol } /** - * @param string $className - * @throws InvalidArgumentException If class name is not a string or does - * not exist. + * @param array $config + * @param class-string $className + * @return array */ - private function validateClassName($className) - { - if (! is_string($className)) { - throw new InvalidArgumentException('Class name must be a string, ' . gettype($className) . ' given'); - } - - if (! class_exists($className) && ! interface_exists($className)) { - throw new InvalidArgumentException('Cannot find class or interface with name ' . $className); - } - } - - /** - * @param string $className - * @return array - */ - private function createInvokable(array $config, $className) + private function createInvokable(array $config, string $className): array { $config[ConfigAbstractFactory::class][$className] = []; return $config; } - /** - * @return array - * @throws InvalidArgumentException If ConfigAbstractFactory configuration - * value is not an array. - */ - public function createFactoryMappingsFromConfig(array $config) + public function createFactoryMappingsFromConfig(array $config): array { if (! array_key_exists(ConfigAbstractFactory::class, $config)) { return $config; @@ -151,22 +132,16 @@ public function createFactoryMappingsFromConfig(array $config) ); } - foreach ($config[ConfigAbstractFactory::class] as $className => $dependency) { + foreach (array_keys($config[ConfigAbstractFactory::class]) as $className) { $config = $this->createFactoryMappings($config, $className); } return $config; } - /** - * @param string $className - * @return array - */ - public function createFactoryMappings(array $config, $className) + public function createFactoryMappings(array $config, string $className): array { - $this->validateClassName($className); - if ( - array_key_exists('service_manager', $config) + array_key_exists($this->serviceManagerConfigurationKey, $config) && array_key_exists('factories', $config['service_manager']) && array_key_exists($className, $config['service_manager']['factories']) ) { @@ -177,26 +152,22 @@ public function createFactoryMappings(array $config, $className) return $config; } - /** - * @return string - */ - public function dumpConfigFile(array $config) + public function dumpConfigFile(array $config): string { $prepared = $this->prepareConfig($config); return sprintf( self::CONFIG_TEMPLATE, - static::class, + self::class, date('Y-m-d H:i:s'), $prepared ); } /** - * @param array|Traversable $config - * @param int $indentLevel - * @return string + * @param int<0,max> $indentLevel + * @return non-empty-string */ - private function prepareConfig($config, $indentLevel = 1) + private function prepareConfig(iterable $config, int $indentLevel = 1): string { $indent = str_repeat(' ', $indentLevel * 4); $entries = []; @@ -205,7 +176,7 @@ private function prepareConfig($config, $indentLevel = 1) $entries[] = sprintf( '%s%s%s,', $indent, - $key ? sprintf('%s => ', $key) : '', + $key !== null ? sprintf('%s => ', $key) : '', $this->createConfigValue($value, $indentLevel) ); } @@ -220,10 +191,9 @@ private function prepareConfig($config, $indentLevel = 1) } /** - * @param string|int|null $key - * @return null|string + * @return non-empty-string|null */ - private function createConfigKey($key) + private function createConfigKey(string|int|null $key): string|null { if (is_string($key) && class_exists($key)) { return sprintf('\\%s::class', $key); @@ -237,12 +207,11 @@ private function createConfigKey($key) } /** - * @param int $indentLevel - * @return string + * @param int<0,max> $indentLevel */ - private function createConfigValue(mixed $value, $indentLevel) + private function createConfigValue(mixed $value, int $indentLevel): string { - if (is_array($value) || $value instanceof Traversable) { + if (is_iterable($value)) { return $this->prepareConfig($value, $indentLevel + 1); } diff --git a/src/Tool/ConfigDumperCommand.php b/src/Tool/ConfigDumperCommand.php deleted file mode 100644 index f9747868..00000000 --- a/src/Tool/ConfigDumperCommand.php +++ /dev/null @@ -1,244 +0,0 @@ -, - * class: string, - * ignoreUnresolved: bool - * } - */ -class ConfigDumperCommand -{ - public const COMMAND_DUMP = 'dump'; - public const COMMAND_ERROR = 'error'; - public const COMMAND_HELP = 'help'; - - public const DEFAULT_SCRIPT_NAME = self::class; - - public const HELP_TEMPLATE = <<Usage: - - %s [-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. -EOH; - - private ConsoleHelper $helper; - - /** - * @param string $scriptName - */ - public function __construct(private $scriptName = self::DEFAULT_SCRIPT_NAME, ?ConsoleHelper $helper = null) - { - $this->helper = $helper ?: new ConsoleHelper(); - } - - /** - * @param array $args Argument list, minus script name - * @return int Exit status - */ - public function __invoke(array $args) - { - $arguments = $this->parseArgs($args); - - switch ($arguments->command) { - case self::COMMAND_HELP: - $this->help(); - return 0; - case self::COMMAND_ERROR: - $this->helper->writeErrorMessage($arguments->message); - $this->help(STDERR); - return 1; - case self::COMMAND_DUMP: - // fall-through - default: - break; - } - - $dumper = new ConfigDumper(); - try { - $config = $dumper->createDependencyConfig( - $arguments->config, - $arguments->class, - $arguments->ignoreUnresolved - ); - } catch (Exception\InvalidArgumentException $e) { - $this->helper->writeErrorMessage(sprintf( - 'Unable to create config for "%s": %s', - $arguments->class, - $e->getMessage() - )); - $this->help(STDERR); - return 1; - } - - file_put_contents($arguments->configFile, $dumper->dumpConfigFile($config)); - - $this->helper->writeLine(sprintf( - '[DONE] Changes written to %s', - $arguments->configFile - )); - return 0; - } - - /** - * @return object - */ - private function parseArgs(array $args) - { - if (! $args) { - return $this->createHelpArgument(); - } - - $arg1 = array_shift($args); - - if (in_array($arg1, ['-h', '--help', 'help'], true)) { - return $this->createHelpArgument(); - } - - $ignoreUnresolved = false; - if (in_array($arg1, ['-i', '--ignore-unresolved'], true)) { - $ignoreUnresolved = true; - $arg1 = array_shift($args); - } - - if (! $args) { - return $this->createErrorArgument('Missing class name'); - } - - $configFile = $arg1; - switch (file_exists($configFile)) { - case true: - $config = require $configFile; - - if (! is_array($config)) { - return $this->createErrorArgument(sprintf( - 'Configuration at path "%s" does not return an array.', - $configFile - )); - } - - break; - case false: - // fall-through - default: - if (! is_writable(dirname($configFile))) { - return $this->createErrorArgument(sprintf( - 'Cannot create configuration at path "%s"; not writable.', - $configFile - )); - } - - $config = []; - break; - } - - $class = array_shift($args); - - if (! class_exists($class)) { - return $this->createErrorArgument(sprintf( - 'Class "%s" does not exist or could not be autoloaded.', - $class - )); - } - - return $this->createArguments(self::COMMAND_DUMP, $configFile, $config, $class, $ignoreUnresolved); - } - - /** - * @param resource $resource Defaults to STDOUT - * @return void - */ - private function help($resource = STDOUT) - { - $this->helper->writeLine(sprintf( - self::HELP_TEMPLATE, - $this->scriptName - ), true, $resource); - } - - /** - * @param string $command - * @param string $configFile File from which config originates, and to - * which it will be written. - * @param array $config Parsed configuration. - * @param string $class Name of class to reflect. - * @param bool $ignoreUnresolved If to ignore classes with unresolved direct dependencies. - * @return ArgumentObject - */ - private function createArguments($command, $configFile, $config, $class, $ignoreUnresolved) - { - return (object) [ - 'command' => $command, - 'configFile' => $configFile, - 'config' => $config, - 'class' => $class, - 'ignoreUnresolved' => $ignoreUnresolved, - ]; - } - - /** - * @param string $message - * @return ErrorObject - */ - private function createErrorArgument($message) - { - return (object) [ - 'command' => self::COMMAND_ERROR, - 'message' => $message, - ]; - } - - /** - * @return HelpObject - */ - private function createHelpArgument() - { - return (object) [ - 'command' => self::COMMAND_HELP, - ]; - } -} diff --git a/src/Tool/ConfigDumperFactory.php b/src/Tool/ConfigDumperFactory.php new file mode 100644 index 00000000..bca8da70 --- /dev/null +++ b/src/Tool/ConfigDumperFactory.php @@ -0,0 +1,34 @@ +isCommandExecutedInMezzioApplication($container)) { + return new ConfigDumper($container, ConfigDumper::MEZZIO_CONTAINER_CONFIGURATION); + } + + return new ConfigDumper($container, ConfigDumper::LAMINAS_MVC_SERVICEMANAGER_CONFIGURATION); + } + + private function isCommandExecutedInMezzioApplication(ContainerInterface $container): bool + { + /** + * @psalm-suppress UndefinedClass MixedArgument We can't require mezzio due to the amount of additional + * dependencies we would have to add here. + */ + return class_exists(Application::class) && $container->has(Application::class); + } +} diff --git a/src/Tool/ConfigDumperInterface.php b/src/Tool/ConfigDumperInterface.php new file mode 100644 index 00000000..9c750e3b --- /dev/null +++ b/src/Tool/ConfigDumperInterface.php @@ -0,0 +1,38 @@ + $config + * @param class-string $className + * @return array + * @throws InvalidArgumentException For unsupported class-string. + */ + public function createDependencyConfig(array $config, string $className, bool $ignoreUnresolved = false): array; + + /** + * @param array $config + * @return array + * @throws InvalidArgumentException If ConfigAbstractFactory configuration + * value is not an array. + */ + public function createFactoryMappingsFromConfig(array $config): array; + + /** + * @param array $config + * @param class-string $className + * @return array + */ + public function createFactoryMappings(array $config, string $className): array; + + /** + * @return non-empty-string + */ + public function dumpConfigFile(array $config): string; +} diff --git a/src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php new file mode 100644 index 00000000..6e1830e0 --- /dev/null +++ b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolver.php @@ -0,0 +1,175 @@ +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 + && in_array($type->getName(), ['array', ArrayAccess::class], true) + ) { + 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 === 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/ConstructorParameterResolver/ConstructorParameterResolverInterface.php b/src/Tool/ConstructorParameterResolver/ConstructorParameterResolverInterface.php new file mode 100644 index 00000000..45be5474 --- /dev/null +++ b/src/Tool/ConstructorParameterResolver/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/ConstructorParameterResolver/FallbackConstructorParameter.php b/src/Tool/ConstructorParameterResolver/FallbackConstructorParameter.php new file mode 100644 index 00000000..c36cc438 --- /dev/null +++ b/src/Tool/ConstructorParameterResolver/FallbackConstructorParameter.php @@ -0,0 +1,13 @@ +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) ); } + /** + * @param class-string $className + * @return non-empty-string + */ private function getClassName(string $className): string { - return substr($className, strrpos($className, '\\') + 1); + $lastNamespaceSeparator = strrpos($className, self::NAMESPACE_SEPARATOR); + if ($lastNamespaceSeparator === false) { + return $className; + } + + $className = substr($className, $lastNamespaceSeparator + 1); + assert($className !== ''); + + return $className; } /** - * @param string $className - * @return array + * @param class-string $className + * @param array $aliases + * @return array */ - private function getConstructorParameters($className) + private function getConstructorParameters(string $className, array $aliases): array { - $reflectionClass = new ReflectionClass($className); - - if (! $reflectionClass->getConstructor()) { - return []; - } - - $constructorParameters = $reflectionClass->getConstructor()->getParameters(); + $dependencies = $this->constructorParameterResolver->resolveConstructorParameterServiceNamesOrFallbackTypes( + $className, + $this->container, + $aliases, + ); - if (empty($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 (empty($constructorParameters)) { - return []; + $stringifiedConstructorArguments[] = $this->export($dependency->argumentValue); } - return array_map(static function (ReflectionParameter $parameter): ?string { - $type = $parameter->getType(); - return $type instanceof ReflectionNamedType && ! $type->isBuiltin() ? $type->getName() : null; - }, $constructorParameters); + return $stringifiedConstructorArguments; } /** - * @param string $className - * @return string + * @param class-string $className + * @param array $aliases */ - private function createArgumentString($className) + 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)); + $arguments = array_map( + static fn(string $dependency): string + => sprintf('%s', $dependency), + $this->getConstructorParameters($className, $aliases) + ); switch (count($arguments)) { case 0: @@ -156,10 +153,40 @@ private function createArgumentString($className) } } - 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/FactoryCreatorCommand.php b/src/Tool/FactoryCreatorCommand.php deleted file mode 100644 index a89df165..00000000 --- a/src/Tool/FactoryCreatorCommand.php +++ /dev/null @@ -1,150 +0,0 @@ -Usage: - - %s [-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. -EOH; - - private ConsoleHelper $helper; - - /** - * @param string $scriptName - */ - public function __construct(private $scriptName = self::DEFAULT_SCRIPT_NAME, ?ConsoleHelper $helper = null) - { - $this->helper = $helper ?: new ConsoleHelper(); - } - - /** - * @param array $args Argument list, minus script name - * @return int Exit status - */ - public function __invoke(array $args) - { - $arguments = $this->parseArgs($args); - - switch ($arguments->command) { - case self::COMMAND_HELP: - $this->help(); - return 0; - case self::COMMAND_ERROR: - assert(is_string($arguments->message)); - $this->helper->writeErrorMessage($arguments->message); - $this->help(STDERR); - return 1; - case self::COMMAND_DUMP: - // fall-through - default: - break; - } - - $generator = new FactoryCreator(); - assert(is_string($arguments->class)); - try { - $factory = $generator->createFactory($arguments->class); - } catch (Exception\InvalidArgumentException $e) { - $this->helper->writeErrorMessage(sprintf( - 'Unable to create factory for "%s": %s', - $arguments->class, - $e->getMessage() - )); - $this->help(STDERR); - return 1; - } - - $this->helper->write($factory, false); - return 0; - } - - /** - * @return ArgumentObject - */ - private function parseArgs(array $args) - { - if (! $args) { - return $this->createArguments(self::COMMAND_HELP); - } - - $arg1 = array_shift($args); - - if (in_array($arg1, ['-h', '--help', 'help'], true)) { - return $this->createArguments(self::COMMAND_HELP); - } - - $class = $arg1; - - if (! class_exists($class)) { - return $this->createArguments(self::COMMAND_ERROR, null, sprintf( - 'Class "%s" does not exist or could not be autoloaded.', - $class - )); - } - - return $this->createArguments(self::COMMAND_DUMP, $class); - } - - /** - * @param resource $resource Defaults to STDOUT - * @return void - */ - private function help($resource = STDOUT) - { - $this->helper->writeLine(sprintf( - self::HELP_TEMPLATE, - $this->scriptName - ), true, $resource); - } - - /** - * @param string $command - * @param string|null $class Name of class to reflect. - * @param string|null $error Error message, if any. - * @return ArgumentObject - */ - private function createArguments($command, $class = null, $error = null) - { - return (object) [ - 'command' => $command, - 'class' => $class, - 'message' => $error, - ]; - } -} diff --git a/src/Tool/FactoryCreatorFactory.php b/src/Tool/FactoryCreatorFactory.php new file mode 100644 index 00000000..f03f9aca --- /dev/null +++ b/src/Tool/FactoryCreatorFactory.php @@ -0,0 +1,23 @@ +get(ConstructorParameterResolverInterface::class); + assert($resolver instanceof ConstructorParameterResolverInterface); + return new FactoryCreator($container, $resolver); + } +} diff --git a/src/Tool/FactoryCreatorInterface.php b/src/Tool/FactoryCreatorInterface.php new file mode 100644 index 00000000..433b9755 --- /dev/null +++ b/src/Tool/FactoryCreatorInterface.php @@ -0,0 +1,15 @@ + $aliases + * @return non-empty-string + */ + public function createFactory(string $className, array $aliases = []): string; +} diff --git a/src/autoload.php b/src/autoload.php deleted file mode 100644 index 76bd64e0..00000000 --- a/src/autoload.php +++ /dev/null @@ -1,21 +0,0 @@ -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 static function nonClassRequestedNames(): array + /** + * @return array + */ + public static 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,202 +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 static function classNamesWithoutConstructorArguments(): array { - $this->container - ->expects(self::exactly(2)) - ->method('has') - ->willReturnMap([ - ['config', false], - [TestAsset\SampleInterface::class, false], - ]) - ->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') - ->willReturnMap([ - ['config', false], - [TestAsset\SampleInterface::class, 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') - ->willReturnMap([ - ['config', false], - ['ValidatorManager', 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') - ->willReturnMap([ - ['config', true], - [TestAsset\SampleInterface::class, true], - ['ValidatorManager', true], - ]); - - $config = ['foo' => 'bar']; - $sample = $this->createMock(TestAsset\SampleInterface::class); - $validators = $this->createMock(TestAsset\ValidatorPluginManager::class); - - $this->container - ->expects(self::exactly(3)) - ->method('get') - ->willReturnMap([ - ['config', $config], - [TestAsset\SampleInterface::class, $sample], - ['ValidatorManager', $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') - ->willReturnMap([ - ['config', false], - [ArrayAccess::class, 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/AbstractFactory/TestAsset/ValidatorPluginManager.php b/test/AbstractFactory/TestAsset/ValidatorPluginManager.php index 05b68562..119c7912 100644 --- a/test/AbstractFactory/TestAsset/ValidatorPluginManager.php +++ b/test/AbstractFactory/TestAsset/ValidatorPluginManager.php @@ -8,4 +8,7 @@ class ValidatorPluginManager extends AbstractPluginManager { + public function validate(mixed $instance): void + { + } } diff --git a/test/AbstractPluginManagerTest.php b/test/AbstractPluginManagerTest.php index 4e02ff94..8a21518a 100644 --- a/test/AbstractPluginManagerTest.php +++ b/test/AbstractPluginManagerTest.php @@ -5,8 +5,7 @@ namespace LaminasTest\ServiceManager; -use Laminas\ServiceManager\ConfigInterface; -use Laminas\ServiceManager\Exception\InvalidArgumentException; +use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\InvalidServiceException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; @@ -14,17 +13,12 @@ use Laminas\ServiceManager\Factory\InvokableFactory; use Laminas\ServiceManager\ServiceManager; use LaminasTest\ServiceManager\TestAsset\InvokableObject; +use LaminasTest\ServiceManager\TestAsset\InvokableObjectPluginManager; use LaminasTest\ServiceManager\TestAsset\SimplePluginManager; -use LaminasTest\ServiceManager\TestAsset\V2v3PluginManager; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use stdClass; -use function restore_error_handler; -use function set_error_handler; - -use const E_USER_DEPRECATED; - /** * @covers \Laminas\ServiceManager\AbstractPluginManager */ @@ -32,7 +26,7 @@ final class AbstractPluginManagerTest extends TestCase { use CommonServiceLocatorBehaviorsTrait; - public static function createContainer(array $config = []): ServiceManager + public static function createContainer(array $config = []): AbstractPluginManager { self::$creationContext = new ServiceManager(); return new TestAsset\LenientPluginManager(self::$creationContext, $config); @@ -126,8 +120,8 @@ public function testReturnsDiscreteInstancesIfOptionsAreProvidedRegardlessOfShar $container = $this->createMock(ContainerInterface::class); $pluginManager = new SimplePluginManager($container, $config); - $first = $pluginManager->get(InvokableObject::class, $options); - $second = $pluginManager->get(InvokableObject::class, $options); + $first = $pluginManager->build(InvokableObject::class, $options); + $second = $pluginManager->build(InvokableObject::class, $options); self::assertInstanceOf(InvokableObject::class, $first); self::assertInstanceOf(InvokableObject::class, $second); @@ -156,7 +150,7 @@ public function testCanWrapCreationInDelegators(): void 'delegators' => [ stdClass::class => [ TestAsset\PreDelegator::class, - static function ($container, $name, $callback) { + static function (ContainerInterface $container, string $name, callable $callback): mixed { $instance = $callback(); $instance->foo = 'bar'; @@ -185,116 +179,12 @@ static function ($container, $name, $callback) { */ public function testGetRaisesExceptionWhenNoFactoryIsResolved(): void { - $pluginManager = $this->createContainer(); + $pluginManager = self::createContainer(); $this->expectException(ServiceNotFoundException::class); $this->expectExceptionMessage($pluginManager::class); $pluginManager->get('Some\Unknown\Service'); } - /** - * @group migration - */ - public function testCallingSetServiceLocatorSetsCreationContextWithDeprecationNotice(): void - { - set_error_handler(static function ($errno, $errstr): void { - self::assertEquals(E_USER_DEPRECATED, $errno); - }, E_USER_DEPRECATED); - $pluginManager = new TestAsset\LenientPluginManager(); - restore_error_handler(); - - self::assertSame($pluginManager, $pluginManager->getCreationContext()); - $serviceManager = new ServiceManager(); - - set_error_handler(static function ($errno, $errstr): void { - self::assertEquals(E_USER_DEPRECATED, $errno); - }, E_USER_DEPRECATED); - $pluginManager->setServiceLocator($serviceManager); - restore_error_handler(); - - self::assertSame($serviceManager, $pluginManager->getCreationContext()); - } - - /** - * @group migration - */ - public function testPassingNoInitialConstructorArgumentSetsPluginManagerAsCreationContextWithDeprecationNotice(): void - { - set_error_handler(static function ($errno, $errstr): void { - self::assertEquals(E_USER_DEPRECATED, $errno); - }, E_USER_DEPRECATED); - $pluginManager = new TestAsset\LenientPluginManager(); - restore_error_handler(); - - self::assertSame($pluginManager, $pluginManager->getCreationContext()); - } - - /** - * @group migration - */ - public function testCanPassConfigInterfaceAsFirstConstructorArgumentWithDeprecationNotice(): void - { - $config = $this->createMock(ConfigInterface::class); - $config - ->expects(self::once()) - ->method('toArray') - ->willReturn([]); - - set_error_handler(static function ($errno, $errstr): void { - self::assertEquals(E_USER_DEPRECATED, $errno); - }, E_USER_DEPRECATED); - $pluginManager = new TestAsset\LenientPluginManager($config); - restore_error_handler(); - - self::assertSame($pluginManager, $pluginManager->getCreationContext()); - } - - public static function invalidConstructorArguments(): array - { - return [ - 'true' => [true], - 'false' => [false], - 'zero' => [0], - 'int' => [1], - 'zero-float' => [0.0], - 'float' => [1.1], - 'string' => ['invalid'], - 'array' => [['invokables' => []]], - 'object' => [(object) ['invokables' => []]], - ]; - } - - /** - * @group migration - * @dataProvider invalidConstructorArguments - */ - public function testPassingNonContainerNonConfigNonNullFirstConstructorArgumentRaisesException(mixed $arg): void - { - $this->expectException(InvalidArgumentException::class); - new TestAsset\LenientPluginManager($arg); - } - - /** - * @group migration - */ - public function testPassingConfigInstanceAsFirstConstructorArgumentSkipsSecondArgumentWithDeprecationNotice(): void - { - $config = $this->createMock(ConfigInterface::class); - $config - ->expects(self::once()) - ->method('toArray') - ->willReturn(['services' => [self::class => $this]]); - - set_error_handler(static function (int $errno, string $_): bool { // phpcs:ignore - self::assertEquals(E_USER_DEPRECATED, $errno); - - return true; - }, E_USER_DEPRECATED); - $pluginManager = new TestAsset\LenientPluginManager($config, ['services' => [self::class => []]]); - restore_error_handler(); - - self::assertSame($this, $pluginManager->get(self::class)); - } - /** * @group migration * @group autoinvokable @@ -330,43 +220,6 @@ public function testPluginManagersMayOptOutOfSupportingAutoInvokableServices(): $pluginManager->get(InvokableObject::class); } - /** - * @group migration - */ - public function testValidateWillFallBackToValidatePluginWhenDefinedAndEmitDeprecationNotice(): void - { - $assertionCalled = false; - $instance = (object) []; - $assertion = static function ($plugin) use ($instance, &$assertionCalled): void { - self::assertSame($instance, $plugin); - $assertionCalled = true; - }; - $pluginManager = new TestAsset\V2ValidationPluginManager(new ServiceManager()); - $pluginManager->assertion = $assertion; - - $errorHandlerCalled = false; - set_error_handler(static function (int $errno, string $errmsg) use (&$errorHandlerCalled): bool { - self::assertEquals(E_USER_DEPRECATED, $errno); - self::assertStringContainsString('3.0', $errmsg); - $errorHandlerCalled = true; - - return true; - }, E_USER_DEPRECATED); - - $pluginManager->validate($instance); - restore_error_handler(); - - self::assertTrue($assertionCalled, 'Assertion was not called by validatePlugin!'); - self::assertTrue($errorHandlerCalled, 'Error handler was not triggered by validatePlugin!'); - } - - public function testSetServiceShouldRaiseExceptionForInvalidPlugin(): void - { - $pluginManager = new SimplePluginManager(new ServiceManager()); - $this->expectException(InvalidServiceException::class); - $pluginManager->setService(stdClass::class, new stdClass()); - } - public function testPassingServiceInstanceViaConfigureShouldRaiseExceptionForInvalidPlugin(): void { $pluginManager = new SimplePluginManager(new ServiceManager()); @@ -407,7 +260,7 @@ public function testAbstractFactoryGetsCreationContext(): void public function testAliasPropertyResolves(): void { - $pluginManager = new V2v3PluginManager(new ServiceManager()); + $pluginManager = new InvokableObjectPluginManager(new ServiceManager()); self::assertInstanceOf(InvokableObject::class, $pluginManager->get('foo')); } diff --git a/test/Command/AheadOfTimeFactoryCreatorCommandTest.php b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php new file mode 100644 index 00000000..194c76fe --- /dev/null +++ b/test/Command/AheadOfTimeFactoryCreatorCommandTest.php @@ -0,0 +1,296 @@ +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 static 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([]); + + self::assertSame(0, $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.'); + + self::assertSame(0, $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, + )); + + 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)); + } +} diff --git a/test/Command/ConfigDumperCommandTest.php b/test/Command/ConfigDumperCommandTest.php new file mode 100644 index 00000000..1151066d --- /dev/null +++ b/test/Command/ConfigDumperCommandTest.php @@ -0,0 +1,222 @@ +configDir = vfsStream::setup('project'); + $this->configDumper = $this->createMock(ConfigDumperInterface::class); + $this->input = $this->createMock(InputInterface::class); + $this->output = $this->createMock(OutputInterface::class); + + $this->command = new ConfigDumperCommand($this->configDumper); + } + + public function assertErrorRaised(string $message): void + { + $this->output + ->expects(self::once()) + ->method('writeln') + ->with(self::stringContains(sprintf('%s', $message))); + } + + public function ignoreUnresolvedArguments(): array + { + return [ + 'short' => ['-i'], + 'long' => ['--ignore-unresolved'], + ]; + } + + public function testRaisesExceptionIfConfigFileNotFoundAndDirectoryNotWritable(): void + { + vfsStream::newDirectory('config', 0550) + ->at($this->configDir); + $config = vfsStream::url('project/config/test.config.php'); + + $this->assertErrorRaised(sprintf('Cannot create configuration at path "%s"; not writable.', $config)); + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', 'Not\A\Real\Class'], + ['configFile', $config], + ]); + self::assertEquals(1, $this->command->run($this->input, $this->output)); + } + + public function testGeneratesConfigFileWhenProvidedConfigurationFileNotFound(): void + { + vfsStream::newDirectory('config', 0775) + ->at($this->configDir); + $config = vfsStream::url('project/config/test.config.php'); + + $this->output + ->expects(self::once()) + ->method('writeln') + ->with('[DONE] Changes written to ' . $config); + + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', SimpleDependencyObject::class], + ['configFile', $config], + ]); + + $this->configDumper + ->expects(self::once()) + ->method('createDependencyConfig') + ->with([], SimpleDependencyObject::class, false) + ->willReturn(['config' => 'value']); + + $this->configDumper + ->expects(self::once()) + ->method('dumpConfigFile') + ->with(['config' => 'value']) + ->willReturn('yada yada'); + + self::assertEquals(0, $this->command->run($this->input, $this->output)); + self::assertSame('yada yada', file_get_contents($config)); + } + + public function testGeneratesConfigFileIgnoringUnresolved(): void + { + vfsStream::newDirectory('config', 0775) + ->at($this->configDir); + $config = vfsStream::url('project/config/test.config.php'); + + $this->output + ->expects(self::once()) + ->method('writeln') + ->with('[DONE] Changes written to ' . $config); + + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', SimpleDependencyObject::class], + ['configFile', $config], + ]); + + $this->input + ->expects(self::once()) + ->method('hasOption') + ->with('ignore-unresolved') + ->willReturn(true); + + $this->configDumper + ->expects(self::once()) + ->method('createDependencyConfig') + ->with([], SimpleDependencyObject::class, true) + ->willReturn(['config' => 'value']); + + $this->configDumper + ->expects(self::once()) + ->method('dumpConfigFile') + ->with(['config' => 'value']) + ->willReturn('yada yada'); + + self::assertEquals(0, $this->command->run($this->input, $this->output)); + self::assertSame('yada yada', file_get_contents($config)); + } + + public function testEmitsErrorWhenConfigurationFileDoesNotReturnArray(): void + { + vfsStream::newFile('config/invalid.config.php') + ->at($this->configDir) + ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/invalid.config.php'))); + $config = vfsStream::url('project/config/invalid.config.php'); + + $this->configDumper + ->expects(self::never()) + ->method('createDependencyConfig'); + + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', SimpleDependencyObject::class], + ['configFile', $config], + ]); + + $this->assertErrorRaised('Configuration at path "' . $config . '" does not return an array.'); + self::assertEquals(1, $this->command->run($this->input, $this->output)); + } + + public function testEmitsErrorWhenClassDoesNotExist(): void + { + vfsStream::newFile('config/test.config.php') + ->at($this->configDir) + ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/test.config.php'))); + $config = vfsStream::url('project/config/test.config.php'); + + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', 'Not\A\Real\Class'], + ['configFile', $config], + ]); + + $this->assertErrorRaised('Class "Not\\A\\Real\\Class" does not exist or could not be autoloaded.'); + self::assertEquals(1, $this->command->run($this->input, $this->output)); + } + + public function testEmitsErrorWhenUnableToCreateConfiguration(): void + { + vfsStream::newFile('config/test.config.php') + ->at($this->configDir) + ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/test.config.php'))); + $config = vfsStream::url('project/config/test.config.php'); + + $this->input + ->method('getArgument') + ->willReturnMap([ + ['class', ObjectWithScalarDependency::class], + ['configFile', $config], + ]); + + $this->configDumper + ->expects(self::once()) + ->method('createDependencyConfig') + ->with([], ObjectWithScalarDependency::class) + ->willThrowException(new InvalidArgumentException('Whatever')); + + $this->assertErrorRaised('Unable to create config for "' . ObjectWithScalarDependency::class . '":'); + self::assertEquals(1, $this->command->run($this->input, $this->output)); + } +} diff --git a/test/Command/FactoryCreatorCommandTest.php b/test/Command/FactoryCreatorCommandTest.php new file mode 100644 index 00000000..ac29844b --- /dev/null +++ b/test/Command/FactoryCreatorCommandTest.php @@ -0,0 +1,122 @@ +factoryCreator = $this->createMock(FactoryCreatorInterface::class); + $this->input = $this->createMock(InputInterface::class); + $this->output = $this->createMock(OutputInterface::class); + $this->command = new FactoryCreatorCommand($this->factoryCreator); + } + + /** + * @return array + */ + public static function invalidArguments(): array + { + return [ + 'string' => ['string'], + 'interface' => [FactoryInterface::class], + ]; + } + + /** + * @dataProvider invalidArguments + */ + public function testEmitsErrorMessageIfArgumentIsNotAClass(string $argument): void + { + $this->input + ->method('getArgument') + ->with('className') + ->willReturn($argument); + + $this->factoryCreator + ->expects(self::never()) + ->method(self::anything()); + + $this->assertErrorRaised(sprintf('Class "%s" does not exist', $argument)); + self::assertSame(1, $this->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 testEmitsErrorWhenUnableToCreateFactory(): void + { + $this->assertErrorRaised('Unable to create factory for "' . ObjectWithScalarDependency::class . '":'); + $this->input + ->method('getArgument') + ->with('className') + ->willReturn(ObjectWithScalarDependency::class); + $this->factoryCreator + ->expects(self::once()) + ->method('createFactory') + ->with(ObjectWithScalarDependency::class) + ->willThrowException(new InvalidArgumentException('Foo bar')); + + self::assertSame(1, $this->command->run($this->input, $this->output)); + } + + public function testEmitsFactoryFileToStdoutWhenSuccessful(): void + { + $expected = file_get_contents(__DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'); + + $this->input + ->method('getArgument') + ->with('className') + ->willReturn(SimpleDependencyObject::class); + + $this->factoryCreator + ->expects(self::once()) + ->method('createFactory') + ->with(SimpleDependencyObject::class) + ->willReturn($expected); + + $this->output + ->expects(self::once()) + ->method('writeln') + ->with($expected); + + $this->assertSame(0, $this->command->run($this->input, $this->output)); + } +} diff --git a/test/CommonServiceLocatorBehaviorsTrait.php b/test/CommonServiceLocatorBehaviorsTrait.php index d8fef3eb..1aaabfd9 100644 --- a/test/CommonServiceLocatorBehaviorsTrait.php +++ b/test/CommonServiceLocatorBehaviorsTrait.php @@ -5,10 +5,9 @@ namespace LaminasTest\ServiceManager; use DateTime; -use Laminas\ServiceManager\ConfigInterface; +use Laminas\ServiceManager\AbstractPluginManager; use Laminas\ServiceManager\Exception\ContainerModificationsNotAllowedException; use Laminas\ServiceManager\Exception\CyclicAliasException; -use Laminas\ServiceManager\Exception\InvalidArgumentException; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; use Laminas\ServiceManager\Factory\FactoryInterface; @@ -28,24 +27,18 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use ReflectionProperty; use stdClass; use function array_fill_keys; use function array_keys; use function array_merge; -use function PHPUnit\Framework\assertIsBool; -use function PHPUnit\Framework\assertIsString; -use function restore_error_handler; -use function set_error_handler; - -use const E_USER_DEPRECATED; +use function assert; +use function in_array; /** - * @see ConfigInterface * @see TestCase * - * @psalm-import-type ServiceManagerConfigurationType from ConfigInterface + * @psalm-import-type ServiceManagerConfiguration from ServiceManager * @psalm-require-extends TestCase */ trait CommonServiceLocatorBehaviorsTrait @@ -53,18 +46,16 @@ trait CommonServiceLocatorBehaviorsTrait /** * The creation context container; used in some mocks for comparisons; set during createContainer. */ - protected static ServiceManager|null $creationContext; + protected static ContainerInterface|null $creationContext; /** - * @psalm-param ServiceManagerConfigurationType $config - * @return ServiceManager - * @todo This will need to be static for future versions of PHPUnit + * @psalm-param ServiceManagerConfiguration $config */ - abstract public static function createContainer(array $config = []); + abstract public static function createContainer(array $config = []): ServiceLocatorInterface; public function testIsSharedByDefault(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -78,7 +69,7 @@ public function testIsSharedByDefault(): void public function testCanDisableSharedByDefault(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -93,7 +84,7 @@ public function testCanDisableSharedByDefault(): void public function testCanDisableSharedForSingleService(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -110,7 +101,7 @@ public function testCanDisableSharedForSingleService(): void public function testCanEnableSharedForSingleService(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -128,7 +119,7 @@ public function testCanEnableSharedForSingleService(): void public function testCanBuildObjectWithInvokableFactory(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ InvokableObject::class => InvokableFactory::class, ], @@ -142,9 +133,9 @@ public function testCanBuildObjectWithInvokableFactory(): void public function testCanCreateObjectWithClosureFactory(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ - stdClass::class => static function (ServiceLocatorInterface $serviceLocator, $className): stdClass { + stdClass::class => static function (ContainerInterface $container, $className): stdClass { self::assertEquals(stdClass::class, $className); return new stdClass(); @@ -159,7 +150,7 @@ public function testCanCreateObjectWithClosureFactory(): void public function testCanCreateServiceWithAbstractFactory(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'abstract_factories' => [ new SimpleAbstractFactory(), ], @@ -175,7 +166,7 @@ public function testAllowsMultipleInstancesOfTheSameAbstractFactory(): void $obj1 = new CallTimesAbstractFactory(); $obj2 = new CallTimesAbstractFactory(); - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'abstract_factories' => [ $obj1, $obj2, @@ -192,7 +183,7 @@ public function testWillReUseAnExistingNamedAbstractFactoryInstance(): void { CallTimesAbstractFactory::setCallTimes(0); - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'abstract_factories' => [ CallTimesAbstractFactory::class, CallTimesAbstractFactory::class, @@ -206,7 +197,7 @@ public function testWillReUseAnExistingNamedAbstractFactoryInstance(): void public function testCanCreateServiceWithAlias(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ InvokableObject::class => InvokableFactory::class, ], @@ -225,7 +216,7 @@ public function testCanCreateServiceWithAlias(): void public function testCheckingServiceExistenceWithChecksAgainstAbstractFactories(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -240,7 +231,7 @@ public function testCheckingServiceExistenceWithChecksAgainstAbstractFactories() public function testBuildNeverSharesInstances(): void { - $serviceManager = $this->createContainer([ + $container = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -249,17 +240,22 @@ public function testBuildNeverSharesInstances(): void ], ]); - $object1 = $serviceManager->build(stdClass::class); - $object2 = $serviceManager->build(stdClass::class, ['foo' => 'bar']); + $object1 = $container->build(stdClass::class); + self::assertInstanceOf(stdClass::class, $object1); + $object2 = $container->build(stdClass::class, ['foo' => 'bar']); + self::assertInstanceOf(stdClass::class, $object2); + $object3 = $container->get(stdClass::class); + self::assertInstanceOf(stdClass::class, $object3); self::assertNotSame($object1, $object2); + self::assertNotSame($object1, $object3); } public function testInitializersAreRunAfterCreation(): void { $initializer = $this->createMock(InitializerInterface::class); - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -281,7 +277,7 @@ public function testInitializersAreRunAfterCreation(): void public function testThrowExceptionIfServiceCannotBeCreated(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => FailingFactory::class, ], @@ -294,7 +290,7 @@ public function testThrowExceptionIfServiceCannotBeCreated(): void public function testThrowExceptionWithStringAsCodeIfServiceCannotBeCreated(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => FailingExceptionWithStringAsCodeFactory::class, ], @@ -307,7 +303,7 @@ public function testThrowExceptionWithStringAsCodeIfServiceCannotBeCreated(): vo public function testConfigureCanAddNewServices(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ DateTime::class => InvokableFactory::class, ], @@ -333,7 +329,7 @@ public function testConfigureCanOverridePreviousSettings(): void $firstFactory = $this->createMock(FactoryInterface::class); $secondFactory = $this->createMock(FactoryInterface::class); - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ DateTime::class => $firstFactory, ], @@ -366,7 +362,7 @@ public function testConfigureInvokablesTakePrecedenceOverFactories(): void { $firstFactory = $this->createMock(FactoryInterface::class); - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'aliases' => [ 'custom_alias' => DateTime::class, ], @@ -390,7 +386,7 @@ public function testConfigureInvokablesTakePrecedenceOverFactories(): void */ public function testHasReturnsFalseIfServiceNotConfigured(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -404,7 +400,7 @@ public function testHasReturnsFalseIfServiceNotConfigured(): void */ public function testHasReturnsTrueIfServiceIsConfigured(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'services' => [ stdClass::class => new stdClass(), ], @@ -418,7 +414,7 @@ public function testHasReturnsTrueIfServiceIsConfigured(): void */ public function testHasReturnsTrueIfFactoryIsConfigured(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -444,7 +440,7 @@ public function testHasChecksAgainstAbstractFactories( AbstractFactoryInterface $abstractFactory, bool $expected, ): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'abstract_factories' => [ $abstractFactory, ], @@ -458,7 +454,7 @@ public function testHasChecksAgainstAbstractFactories( */ public function testCanConfigureAllServiceTypes(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'services' => [ 'config' => ['foo' => 'bar'], ], @@ -540,7 +536,7 @@ static function (ContainerInterface $container, $instance): void { */ public function testCanSpecifyAbstractFactoryUsingStringViaConfiguration(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'abstract_factories' => [ SimpleAbstractFactory::class, ], @@ -551,50 +547,9 @@ public function testCanSpecifyAbstractFactoryUsingStringViaConfiguration(): void self::assertInstanceOf(DateTime::class, $dateTime); } - public static function invalidFactories(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'zero' => [0], - 'int' => [1], - 'zero-float' => [0.0], - 'float' => [1.1], - 'array' => [['foo', 'bar']], - 'non-invokable-object' => [(object) ['foo' => 'bar']], - ]; - } - - public static function invalidAbstractFactories(): array - { - $factories = self::invalidFactories(); - $factories['non-class-string'] = ['non-callable-string', 'valid class name']; - - return $factories; - } - - /** - * @dataProvider invalidAbstractFactories - * @covers \Laminas\ServiceManager\ServiceManager::configure - */ - public function testPassingInvalidAbstractFactoryTypeViaConfigurationRaisesException( - mixed $factory, - string $contains = 'invalid abstract factory' - ): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage($contains); - /** @psalm-suppress MixedArgumentTypeCoercion */ - $this->createContainer([ - 'abstract_factories' => [ - $factory, - ], - ]); - } - public function testCanSpecifyInitializerUsingStringViaConfiguration(): void { - $serviceManager = $this->createContainer([ + $serviceManager = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -610,80 +565,24 @@ public function testCanSpecifyInitializerUsingStringViaConfiguration(): void self::assertEquals('bar', $instance->foo, '"foo" property was not properly injected'); } - public static function invalidInitializers(): array - { - $factories = self::invalidFactories(); - $factories['non-class-string'] = ['non-callable-string', 'callable or an instance of']; - - return $factories; - } - - /** - * @dataProvider invalidInitializers - * @covers \Laminas\ServiceManager\ServiceManager::configure - */ - public function testPassingInvalidInitializerTypeViaConfigurationRaisesException( - mixed $initializer, - string $contains = 'invalid initializer' - ): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage($contains); - /** @psalm-suppress MixedArgumentTypeCoercion */ - $this->createContainer(['initializers' => [$initializer]]); - } - /** * @covers \Laminas\ServiceManager\ServiceManager::getFactory */ public function testGetRaisesExceptionWhenNoFactoryIsResolved(): void { - $serviceManager = $this->createContainer(); + $serviceManager = self::createContainer(); $this->expectException(ContainerExceptionInterface::class); $this->expectExceptionMessage('Unable to resolve'); $serviceManager->get('Some\Unknown\Service'); } - public static function invalidDelegators(): array - { - $invalidDelegators = self::invalidFactories(); - $invalidDelegators['invalid-classname'] = ['not-a-class-name', 'invalid delegator']; - $invalidDelegators['non-invokable-class'] = [stdClass::class]; - - return $invalidDelegators; - } - - /** - * @dataProvider invalidDelegators - * @covers \Laminas\ServiceManager\ServiceManager::createDelegatorFromName - */ - public function testInvalidDelegatorShouldRaiseExceptionDuringCreation( - mixed $delegator, - string $contains = 'non-callable delegator' - ): void { - /** @psalm-suppress MixedArgumentTypeCoercion */ - $serviceManager = $this->createContainer([ - 'factories' => [ - stdClass::class => InvokableFactory::class, - ], - 'delegators' => [ - stdClass::class => [ - $delegator, - ], - ], - ]); - - $this->expectException(ServiceNotCreatedException::class); - $this->expectExceptionMessage($contains); - $serviceManager->get(stdClass::class); - } - /** * @group mutation * @covers \Laminas\ServiceManager\ServiceManager::setAlias */ public function testCanInjectAliases(): void { - $container = $this->createContainer([ + $container = self::createContainer([ 'factories' => [ 'foo' => static fn (): stdClass => new stdClass(), ], @@ -705,7 +604,7 @@ public function testCanInjectAliases(): void */ public function testCanInjectInvokables(): void { - $container = $this->createContainer(); + $container = self::createContainer(); $container->setInvokableClass('foo', stdClass::class); self::assertTrue($container->has('foo')); @@ -723,7 +622,7 @@ public function testCanInjectInvokables(): void public function testCanInjectFactories(): void { $instance = new stdClass(); - $container = $this->createContainer(); + $container = self::createContainer(); $container->setFactory('foo', static fn (): stdClass => $instance); @@ -734,35 +633,16 @@ public function testCanInjectFactories(): void self::assertSame($instance, $foo); } - /** - * @group mutation - * @covers \Laminas\ServiceManager\ServiceManager::mapLazyService - */ - public function testCanMapLazyServices(): void - { - $container = $this->createContainer(); - $container->mapLazyService('foo', self::class); - $r = new ReflectionProperty($container, 'lazyServices'); - $lazyServices = $r->getValue($container); - - self::assertIsArray($lazyServices); - self::assertArrayHasKey('class_map', $lazyServices); - self::assertIsArray($lazyServices['class_map']); - self::assertArrayHasKey('foo', $lazyServices['class_map']); - self::assertEquals(self::class, $lazyServices['class_map']['foo']); - } - /** * @group mutation * @covers \Laminas\ServiceManager\ServiceManager::addAbstractFactory */ public function testCanInjectAbstractFactories(): void { - $container = $this->createContainer(); + $container = self::createContainer(); $container->addAbstractFactory(SimpleAbstractFactory::class); - // @todo Remove "true" flag once #49 is merged - self::assertTrue($container->has(stdClass::class, true)); + self::assertTrue($container->has(stdClass::class)); $instance = $container->get(stdClass::class); @@ -775,7 +655,7 @@ public function testCanInjectAbstractFactories(): void */ public function testCanInjectDelegators(): void { - $container = $this->createContainer([ + $container = self::createContainer([ 'factories' => [ 'foo' => static fn (): stdClass => new stdClass(), ], @@ -800,14 +680,14 @@ public function testCanInjectDelegators(): void */ public function testCanInjectInitializers(): void { - $container = $this->createContainer([ + $container = self::createContainer([ 'factories' => [ 'foo' => static fn (): stdClass => new stdClass(), ], ]); $container->addInitializer(static function (ContainerInterface $container, $instance) { if (! $instance instanceof stdClass) { - return; + return $instance; } $instance->name = stdClass::class; @@ -827,7 +707,7 @@ public function testCanInjectInitializers(): void */ public function testCanInjectServices(): void { - $container = $this->createContainer(); + $container = self::createContainer(); $container->setService('foo', $this); self::assertSame($this, $container->get('foo')); @@ -839,7 +719,7 @@ public function testCanInjectServices(): void */ public function testCanInjectSharingRules(): void { - $container = $this->createContainer([ + $container = self::createContainer([ 'factories' => [ 'foo' => static fn (): stdClass => new stdClass(), ], @@ -908,7 +788,7 @@ static function (): void { */ public function testConfiguringInstanceRaisesExceptionIfAllowOverrideIsFalse(string $method, array $args): void { - $container = $this->createContainer(['services' => ['foo' => $this]]); + $container = self::createContainer(['services' => ['foo' => $this]]); $container->setAllowOverride(false); $this->expectException(ContainerModificationsNotAllowedException::class); $container->$method(...$args); @@ -919,7 +799,7 @@ public function testConfiguringInstanceRaisesExceptionIfAllowOverrideIsFalse(str */ public function testAllowOverrideFlagIsFalseByDefault(): ContainerInterface { - $container = $this->createContainer(); + $container = self::createContainer(); self::assertFalse($container->getAllowOverride()); @@ -930,28 +810,13 @@ public function testAllowOverrideFlagIsFalseByDefault(): ContainerInterface * @group mutation * @depends testAllowOverrideFlagIsFalseByDefault */ - public function testAllowOverrideFlagIsMutable(ServiceManager $container): void + public function testAllowOverrideFlagIsMutable(ServiceManager|AbstractPluginManager $container): void { $container->setAllowOverride(true); self::assertTrue($container->getAllowOverride()); } - /** - * @group migration - */ - public function testCanRetrieveParentContainerViaGetServiceLocatorWithDeprecationNotice(): void - { - $container = $this->createContainer(); - set_error_handler(static function (int $errno): bool { - self::assertEquals(E_USER_DEPRECATED, $errno); - - return true; - }, E_USER_DEPRECATED); - self::assertSame(self::$creationContext, $container->getServiceLocator()); - restore_error_handler(); - } - /** * @group zendframework/zend-servicemanager#83 */ @@ -959,7 +824,7 @@ public function testCrashesOnCyclicAliases(): void { $this->expectException(CyclicAliasException::class); - $this->createContainer([ + self::createContainer([ 'aliases' => [ 'a' => 'b', 'b' => 'a', @@ -969,7 +834,7 @@ public function testCrashesOnCyclicAliases(): void public function testMinimalCyclicAliasDefinitionShouldThrow(): void { - $sm = $this->createContainer([]); + $sm = self::createContainer([]); $this->expectException(CyclicAliasException::class); $sm->setAlias('alias', 'alias'); @@ -977,7 +842,7 @@ public function testMinimalCyclicAliasDefinitionShouldThrow(): void public function testCoverageDepthFirstTaggingOnRecursiveAliasDefinitions(): void { - $sm = $this->createContainer([ + $sm = self::createContainer([ 'factories' => [ stdClass::class => InvokableFactory::class, ], @@ -1002,6 +867,7 @@ public function testCoverageDepthFirstTaggingOnRecursiveAliasDefinitions(): void * all internal states, thereby verifying that build/get/has * remain stable through the internal states. * + * @param list $test * @dataProvider provideConsistencyOverInternalStatesTests */ public function testConsistencyOverInternalStates( @@ -1010,19 +876,28 @@ public function testConsistencyOverInternalStates( array $test, bool $shared ): void { - $sm = clone $smTemplate; - $object['get'] = []; - $object['build'] = []; + $sm = clone $smTemplate; + $object = [ + 'get' => [], + 'build' => [], + ]; // call get()/build() and store the retrieved // objects in $object['get'] or $object['build'] // respectively foreach ($test as $method) { - $obj = $sm->$method($name); - $object[$shared ? $method : 'build'][] = $obj; + assert(in_array($method, ['get', 'build'], true)); + $obj = $sm->$method($name); self::assertNotNull($obj); self::assertTrue($sm->has($name)); + + $target = $method; + if (! $shared) { + $target = 'build'; + } + + $object[$target][] = $obj; } // compares the first to the first also, but ok @@ -1047,7 +922,14 @@ public function testConsistencyOverInternalStates( * * @see testConsistencyOverInternalStates above * - * @return list, 3: bool}> + * @return array< + * array{ + * 0: ContainerInterface, + * 1: string, + * 2: list, + * 3: bool + * } + * > */ public static function provideConsistencyOverInternalStatesTests(): array { @@ -1100,39 +982,38 @@ public static function provideConsistencyOverInternalStatesTests(): array } } - /** @psalm-var list> $callSequences */ - $tests = []; foreach ($configs as $config) { - $smTemplate = self::createContainer($config); + $smTemplate = self::createContainer($config); + $sharedByDefault = $config['shared_by_default'] ?? true; // setup sharing, services are always shared + /** @var array $names */ $names = array_fill_keys(array_keys($config['services']), true); // initialize the other keys with shared_by_default // and merge them - $names = array_merge(array_fill_keys(array_keys(array_merge( - $config['factories'], - $config['invokables'], - $config['aliases'], - $config['delegators'], - )), $config['shared_by_default'] ?? true), $names); - - // add the key resolved by the abstract factory - $names['foo'] = $config['shared_by_default'] ?? true; - - // adjust shared setting for individual keys from - // $shared array if present - if (! empty($config['shared'])) { - foreach ($config['shared'] as $name => $shared) { - $names[$name] = $shared; + foreach ( + array_keys(array_merge( + $config['factories'] ?? [], + $config['invokables'] ?? [], + $config['aliases'] ?? [], + $config['delegators'] ?? [], + )) as $name + ) { + // do not change shared setting for service placed in `services` as services are always shared + if (isset($config['services'][$name])) { + continue; } + + $names[$name] = $sharedByDefault; } + // add the key resolved by the abstract factory + $names['foo'] = $sharedByDefault; + foreach ($names as $name => $shared) { - assertIsString($name); - assertIsBool($shared); foreach ($callSequences as $callSequence) { $tests[] = [$smTemplate, $name, $callSequence, $shared]; } diff --git a/test/ConfigTest.php b/test/ConfigTest.php deleted file mode 100644 index 7a190a49..00000000 --- a/test/ConfigTest.php +++ /dev/null @@ -1,130 +0,0 @@ - [ - 'foo' => TestAsset\InvokableObject::class, - ], - 'delegators' => [ - 'foo' => [ - TestAsset\PreDelegator::class, - ], - ], - 'factories' => [ - 'service' => TestAsset\FactoryObject::class, - ], - ]; - - $configuration = new TestAsset\ExtendedConfig($config); - $result = $configuration->toArray(); - - $expected = [ - 'invokables' => [ - 'foo' => TestAsset\InvokableObject::class, - TestAsset\InvokableObject::class => TestAsset\InvokableObject::class, - ], - 'delegators' => [ - 'foo' => [ - TestAsset\InvokableObject::class, - TestAsset\PreDelegator::class, - ], - ], - 'factories' => [ - 'service' => TestAsset\FactoryObject::class, - ], - ]; - - self::assertEquals($expected, $result); - } - - public function testPassesKnownServiceConfigKeysToServiceManagerWithConfigMethod(): array - { - $expected = [ - 'abstract_factories' => [ - self::class, - __NAMESPACE__, - ], - 'aliases' => [ - 'foo' => self::class, - 'bar' => __NAMESPACE__, - ], - 'delegators' => [ - 'foo' => [ - self::class, - __NAMESPACE__, - ], - ], - 'factories' => [ - 'foo' => self::class, - 'bar' => __NAMESPACE__, - ], - 'initializers' => [ - self::class, - __NAMESPACE__, - ], - 'invokables' => [ - 'foo' => self::class, - 'bar' => __NAMESPACE__, - ], - 'lazy_services' => [ - 'class_map' => [ - self::class => self::class, - __NAMESPACE__ => __NAMESPACE__, - ], - ], - 'services' => [ - 'foo' => $this, - ], - 'shared' => [ - self::class => true, - __NAMESPACE__ => false, - ], - ]; - - $config = $expected + [ - 'foo' => 'bar', - 'baz' => 'bat', - ]; - - $services = $this->createMock(ServiceManager::class); - $services - ->expects(self::once()) - ->method('configure') - ->with($expected) - ->willReturn('CALLED'); - - /** @psalm-suppress ArgumentTypeCoercion Keeping this invalid configuration to ensure BC compatibility. */ - $configuration = new Config($config); - self::assertEquals('CALLED', $configuration->configureServiceManager($services)); - - return [ - 'array' => $expected, - 'config' => $configuration, - ]; - } - - /** - * @depends testPassesKnownServiceConfigKeysToServiceManagerWithConfigMethod - */ - public function testToArrayReturnsConfiguration(array $dependencies): void - { - $configuration = $dependencies['array']; - $configInstance = $dependencies['config']; - - self::assertSame($configuration, $configInstance->toArray()); - } -} diff --git a/test/ExamplePluginManagerTest.php b/test/ExamplePluginManagerTest.php index 60fe0126..a7366b89 100644 --- a/test/ExamplePluginManagerTest.php +++ b/test/ExamplePluginManagerTest.php @@ -4,28 +4,25 @@ namespace LaminasTest\ServiceManager; +use Laminas\ServiceManager\AbstractSingleInstancePluginManager; use Laminas\ServiceManager\ServiceManager; use Laminas\ServiceManager\Test\CommonPluginManagerTrait; use LaminasTest\ServiceManager\TestAsset\InvokableObject; -use LaminasTest\ServiceManager\TestAsset\V2v3PluginManager; +use LaminasTest\ServiceManager\TestAsset\InvokableObjectPluginManager; use PHPUnit\Framework\TestCase; -use RuntimeException; /** * Example test of using CommonPluginManagerTrait + * + * @psalm-import-type ServiceManagerConfiguration from ServiceManager */ final class ExamplePluginManagerTest extends TestCase { use CommonPluginManagerTrait; - protected static function getPluginManager(): V2v3PluginManager + protected static function getPluginManager(array $config = []): AbstractSingleInstancePluginManager { - return new V2v3PluginManager(new ServiceManager()); - } - - protected function getV2InvalidPluginException(): string - { - return RuntimeException::class; + return new InvokableObjectPluginManager(new ServiceManager(), $config); } protected function getInstanceOf(): string diff --git a/test/Exception/CyclicAliasExceptionTest.php b/test/Exception/CyclicAliasExceptionTest.php index cf121ab3..a4c5a0cc 100644 --- a/test/Exception/CyclicAliasExceptionTest.php +++ b/test/Exception/CyclicAliasExceptionTest.php @@ -30,7 +30,7 @@ public function testFromCyclicAlias(string $alias, array $aliases, string $expec * * @return array< * non-empty-string, - * array{0:non-empty-string,1:array,non-empty-string} + * array{0:non-empty-string,1:array,2:non-empty-string} * > */ public static function cyclicAliasProvider(): array @@ -97,7 +97,7 @@ public static function cyclicAliasProvider(): array /** * @dataProvider aliasesProvider - * @param string[] $aliases + * @param array $aliases */ public function testFromAliasesMap(array $aliases, string $expectedMessage): void { @@ -108,7 +108,7 @@ public function testFromAliasesMap(array $aliases, string $expectedMessage): voi } /** - * @return array, string}> + * @return array, 1: string}> */ public static function aliasesProvider(): array { diff --git a/test/Factory/InvokableFactoryTest.php b/test/Factory/InvokableFactoryTest.php index 7bb7d60c..56aeae3e 100644 --- a/test/Factory/InvokableFactoryTest.php +++ b/test/Factory/InvokableFactoryTest.php @@ -4,10 +4,10 @@ namespace LaminasTest\ServiceManager\Factory; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\InvokableFactory; use LaminasTest\ServiceManager\TestAsset\InvokableObject; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; /** * @covers \Laminas\ServiceManager\Factory\InvokableFactory @@ -16,7 +16,7 @@ final class InvokableFactoryTest extends TestCase { public function testCanCreateObject(): void { - $container = $this->createMock(containerinterface::class); + $container = $this->createMock(ContainerInterface::class); $factory = new InvokableFactory(); $object = $factory($container, InvokableObject::class, ['foo' => 'bar']); diff --git a/test/LaminasComponentInstallerIntegrationTest.php b/test/LaminasComponentInstallerIntegrationTest.php new file mode 100644 index 00000000..b6199ab6 --- /dev/null +++ b/test/LaminasComponentInstallerIntegrationTest.php @@ -0,0 +1,106 @@ +getComposerJsonPath(); + if (! is_readable($composerJsonPath)) { + self::fail(sprintf('`composer.json` located at "%s" is not readable.', $composerJsonPath)); + } + + try { + $composerJson = json_decode(file_get_contents($composerJsonPath), true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + self::fail(sprintf( + '`composer.json` located at "%s" is invalid: %s', + $composerJsonPath, + $exception->getMessage() + )); + } + + self::assertIsArray($composerJson); + $composerJsonExtra = $composerJson['extra'] ?? []; + self::assertIsArray($composerJsonExtra); + $composerJsonExtraLaminas = $composerJsonExtra['laminas'] ?? []; + self::assertIsArray($composerJsonExtraLaminas); + + $configProviderClassName = $composerJsonExtraLaminas['config-provider'] ?? null; + $namespaceOfModule = $composerJsonExtraLaminas['module'] ?? null; + + $errorTemplate = 'The `composer.json` is missing "extra.laminas.%s" information.'; + + self::assertIsString($configProviderClassName, sprintf($errorTemplate, 'config-provider')); + self::assertNotEmpty($configProviderClassName, sprintf($errorTemplate, 'config-provider')); + self::assertIsString($namespaceOfModule, sprintf($errorTemplate, 'module')); + self::assertNotEmpty($namespaceOfModule, sprintf($errorTemplate, 'module')); + + return ['config-provider' => $configProviderClassName, 'module' => $namespaceOfModule]; + } + + public function testWillProvideConfigProviderInformationsToComponentInstaller(): void + { + $componentInstallerInformations = $this->parseComposerJsonExtraForLaminasComponentInstallerInformations(); + $configProviderClassName = $this->getConfigProviderClassName(); + self::assertSame($configProviderClassName, $componentInstallerInformations['config-provider']); + } + + public function testWillProvideModuleInformationsToComponentInstaller(): void + { + $componentInstallerInformations = $this->parseComposerJsonExtraForLaminasComponentInstallerInformations(); + + $configProviderClassName = $this->getConfigProviderClassName(); + $reflectionClass = new ReflectionClass($configProviderClassName); + $namespaceOfModule = $reflectionClass->getNamespaceName(); + self::assertSame($namespaceOfModule, $componentInstallerInformations['module']); + $moduleClassName = sprintf('%s\\Module', $componentInstallerInformations['module']); + self::assertTrue( + class_exists($moduleClassName), + sprintf( + 'Module class "%s" could not be found. Did you miss to create it?', + $moduleClassName + ) + ); + } + + /** + * @return class-string + */ + private function getConfigProviderClassName(): string + { + return ConfigProvider::class; + } +} diff --git a/test/Proxy/LazyServiceFactoryTest.php b/test/Proxy/LazyServiceFactoryTest.php index b6ff7feb..87d4541e 100644 --- a/test/Proxy/LazyServiceFactoryTest.php +++ b/test/Proxy/LazyServiceFactoryTest.php @@ -7,7 +7,7 @@ use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; use Laminas\ServiceManager\Proxy\LazyServiceFactory; -use LaminasTest\ServiceManager\TestAsset\CallbackService; +use LaminasTest\ServiceManager\TestAsset\ClassWithCallbackMethod; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ProxyManager\Factory\LazyLoadingValueHolderFactory; @@ -51,7 +51,7 @@ public function testImplementsDelegatorFactoryInterface(): void public function testThrowExceptionWhenServiceNotExists(): void { - $callback = $this->createMock(CallbackService::class); + $callback = $this->createMock(ClassWithCallbackMethod::class); $callback ->expects(self::never()) @@ -69,11 +69,7 @@ public function testThrowExceptionWhenServiceNotExists(): void public function testCreates(): void { - $callback = $this->createMock(CallbackService::class); - $callback - ->expects(self::once()) - ->method('callback') - ->willReturn('fooValue'); + $callback = new ClassWithCallbackMethod('fooValue'); $expectedService = $this->createMock(VirtualProxyInterface::class); $proxy = $this->createMock(LazyLoadingInterface::class); diff --git a/test/ServiceManagerContainerInteropIntegrationTest.php b/test/ServiceManagerContainerInteropIntegrationTest.php deleted file mode 100644 index a4f54ac7..00000000 --- a/test/ServiceManagerContainerInteropIntegrationTest.php +++ /dev/null @@ -1,77 +0,0 @@ -container = new ServiceManager([]); - } - - /** - * NOTE: using try-catch here is to avoid phpunits exception comparison. - * PHPUnit is not directly catching specific exceptions when using {@see TestCase::expectException()} - * but catches {@see Throwable} and does an "instanceof" comparison. - */ - public function testUpstreamCanCatchNotFoundException(): void - { - try { - $this->container->get('unexisting service'); - self::fail('No exception was thrown.'); - } catch (NotFoundException $exception) { - self::assertStringContainsString( - 'Unable to resolve service "unexisting service" to a factory', - $exception->getMessage() - ); - } - } - - /** - * NOTE: using try-catch here is to avoid phpunits exception comparison. - * PHPUnit is not directly catching specific exceptions when using {@see TestCase::expectException()} - * but catches {@see Throwable} and does an "instanceof" comparison. - */ - public function testUpstreamCanCatchContainerException(): void - { - try { - $this->container->get('unexisting service'); - self::fail('No exception was thrown.'); - } catch (ContainerException $exception) { - self::assertStringContainsString( - 'Unable to resolve service "unexisting service" to a factory', - $exception->getMessage() - ); - } - } - - public function testUpstreamCanUseInteropContainerForMethodSignature(): void - { - $factory = new class implements FactoryInterface { - /** @param string $requestedName */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): stdClass - { - return new stdClass(); - } - }; - - $instance = $factory($this->container, stdClass::class); - - self::assertInstanceOf(stdClass::class, $instance); - } -} diff --git a/test/ServiceManagerTest.php b/test/ServiceManagerTest.php index a493c08c..16843e9c 100644 --- a/test/ServiceManagerTest.php +++ b/test/ServiceManagerTest.php @@ -5,7 +5,6 @@ namespace LaminasTest\ServiceManager; use DateTime; -use Laminas\ServiceManager\ConfigInterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; use Laminas\ServiceManager\Factory\FactoryInterface; use Laminas\ServiceManager\Factory\InvokableFactory; @@ -15,20 +14,19 @@ use LaminasTest\ServiceManager\TestAsset\SimpleServiceManager; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use ReflectionProperty; use stdClass; /** - * @see ConfigInterface - * * @covers \Laminas\ServiceManager\ServiceManager - * @psalm-import-type ServiceManagerConfigurationType from ConfigInterface + * @psalm-import-type ServiceManagerConfiguration from ServiceManager */ final class ServiceManagerTest extends TestCase { use CommonServiceLocatorBehaviorsTrait; /** - * @psalm-param ServiceManagerConfigurationType $config + * @psalm-param ServiceManagerConfiguration $config */ public static function createContainer(array $config = []): ServiceManager { @@ -40,7 +38,7 @@ public static function createContainer(array $config = []): ServiceManager public function testServiceManagerIsAPsr11Container(): void { - $container = $this->createContainer(); + $container = self::createContainer(); self::assertInstanceOf(ContainerInterface::class, $container); } @@ -110,7 +108,7 @@ static function (ContainerInterface $container, string $name, callable $callback ]); $instance = $serviceManager->get(stdClass::class); - + self::assertInstanceOf(stdClass::class, $instance); self::assertTrue(isset($instance->option), 'Delegator-injected option was not found'); self::assertEquals( $config['option'], @@ -218,19 +216,13 @@ public function testMapsOneToOneInvokablesAsInvokableFactoriesInternally(): void ], ]; - $serviceManager = new class ($config) extends ServiceManager - { - public function getFactories(): array - { - return $this->factories; - } - }; + $serviceManager = new ServiceManager($config); self::assertSame( [ InvokableObject::class => InvokableFactory::class, ], - $serviceManager->getFactories(), + $this->extractPrivateProperty($serviceManager, 'factories'), 'Invokable object factory not found' ); } @@ -243,24 +235,13 @@ public function testMapsNonSymmetricInvokablesAsAliasPlusInvokableFactory(): voi ], ]; - $serviceManager = new class ($config) extends ServiceManager - { - public function getFactories(): array - { - return $this->factories; - } - - public function getAliases(): array - { - return $this->aliases; - } - }; + $serviceManager = new ServiceManager($config); self::assertSame( [ 'Invokable' => InvokableObject::class, ], - $serviceManager->getAliases(), + $this->extractPrivateProperty($serviceManager, 'aliases'), 'Alias not found for non-symmetric invokable' ); @@ -268,7 +249,7 @@ public function getAliases(): array [ InvokableObject::class => InvokableFactory::class, ], - $serviceManager->getFactories() + $this->extractPrivateProperty($serviceManager, 'factories'), ); } @@ -374,7 +355,7 @@ public function testAbstractFactoryShouldBeCheckedForResolvedAliasesInsteadOfAli $abstractFactory ->expects(self::once()) ->method('canCreate') - ->willReturnCallback(static fn ($context, string $name): bool => $name === 'Alias'); + ->willReturnCallback(static fn (ContainerInterface $context, string $name): bool => $name === 'Alias'); self::assertTrue($serviceManager->has('Alias')); } @@ -412,7 +393,9 @@ public function testResolvedAliasFromAbstractFactory(): void $abstractFactory ->expects(self::exactly(2)) ->method('canCreate') - ->willReturnCallback(static fn ($context, string $name): bool => $name === 'ServiceName'); + ->willReturnCallback( + static fn (ContainerInterface $context, string $name): bool => $name === 'ServiceName' + ); self::assertTrue($serviceManager->has('Alias')); } @@ -434,8 +417,8 @@ public function testResolvedAliasNoMatchingAbstractFactoryReturnsFalse(): void ->expects(self::exactly(2)) ->method('canCreate') ->willReturnMap([ - [self::anything(), 'Alias', false], - [self::anything(), 'ServiceName', false], + [$serviceManager, 'Alias', false], + [$serviceManager, 'ServiceName', false], ]); self::assertFalse($serviceManager->has('Alias')); @@ -450,7 +433,7 @@ public function testConfigureMultipleTimesAvoidsDuplicates(): void { $delegatorFactory = static function ( ContainerInterface $container, - $name, + string $name, callable $callback ): InvokableObject { /** @var InvokableObject $instance */ @@ -521,8 +504,7 @@ static function (object $service) use (&$initializerTwoCalled): object { } /** - * @param array $config - * @psalm-param ServiceManagerConfigurationType $config + * @param ServiceManagerConfiguration $config * @param non-empty-string $serviceName * @param non-empty-string $alias * @dataProvider aliasedServices @@ -541,9 +523,9 @@ public function testWontShareServiceWhenRequestedByAlias(array $config, string $ /** * @psalm-return array */ public static function aliasedServices(): array @@ -583,10 +565,7 @@ public static function aliasedServices(): array [ 'abstract_factories' => [ new class implements AbstractFactoryInterface { - /** - * @param string $requestedName - */ - public function canCreate(ContainerInterface $container, $requestedName): bool + public function canCreate(ContainerInterface $container, string $requestedName): bool { return $requestedName === stdClass::class; } @@ -640,4 +619,28 @@ public function testHasVerifiesAliasesBeforeUsingAbstractFactories(): void self::assertTrue($serviceManager->has('config')); } + + /** + * @group mutation + * @covers \Laminas\ServiceManager\ServiceManager::mapLazyService + */ + public function testCanMapLazyServices(): void + { + $container = self::createContainer(); + + $container->mapLazyService('foo', self::class); + $r = new ReflectionProperty($container, 'lazyServices'); + $lazyServices = $r->getValue($container); + + self::assertIsArray($lazyServices); + self::assertArrayHasKey('class_map', $lazyServices); + self::assertIsArray($lazyServices['class_map']); + self::assertArrayHasKey('foo', $lazyServices['class_map']); + self::assertEquals(self::class, $lazyServices['class_map']['foo']); + } + + private function extractPrivateProperty(object $object, string $propertyName): mixed + { + return (new ReflectionProperty($object, $propertyName))->getValue($object); + } } diff --git a/test/StaticAnalysis/CallablePluginManager.php b/test/StaticAnalysis/CallablePluginManager.php new file mode 100644 index 00000000..fdddacd1 --- /dev/null +++ b/test/StaticAnalysis/CallablePluginManager.php @@ -0,0 +1,51 @@ + + */ +final class CallablePluginManager extends AbstractPluginManager +{ + public function getWhateverPlugin(array|null $options = null): callable + { + if ($options === null) { + return $this->get('foo'); + } + + return $this->build('foo', $options); + } + + public function validateWhateverPlugin(mixed $plugin): callable + { + $this->validate($plugin); + return $plugin; + } + + public function getConcretePlugin(): ConcreteCallablePlugin + { + return self::get(ConcreteCallablePlugin::class); + } + + public function buildConcretePlugin(): ConcreteCallablePlugin + { + return self::build(ConcreteCallablePlugin::class); + } + + /** + * {@inheritDoc} + */ + public function validate(mixed $instance): void + { + if (! is_callable($instance)) { + throw new InvalidArgumentException('Provided instance is not callable.'); + } + } +} diff --git a/test/StaticAnalysis/ConcreteCallablePlugin.php b/test/StaticAnalysis/ConcreteCallablePlugin.php new file mode 100644 index 00000000..e14de0c8 --- /dev/null +++ b/test/StaticAnalysis/ConcreteCallablePlugin.php @@ -0,0 +1,13 @@ + + */ +final class MixedPluginManager extends AbstractPluginManager +{ + public function getWhateverPlugin(array|null $options = null): mixed + { + if ($options === null) { + return $this->get('foo'); + } + + return $this->build('foo', $options); + } + + public function functionValidateWhateverPlugin(mixed $plugin): mixed + { + $this->validate($plugin); + return $plugin; + } + + public function validate(mixed $instance): void + { + } +} diff --git a/test/StaticAnalysis/ServiceLocatorInterfaceConsumer.php b/test/StaticAnalysis/ServiceLocatorInterfaceConsumer.php new file mode 100644 index 00000000..67f00b87 --- /dev/null +++ b/test/StaticAnalysis/ServiceLocatorInterfaceConsumer.php @@ -0,0 +1,65 @@ +getServiceProvider(); + + $date = $serviceProvider->get(DateTimeImmutable::class); + echo $date->format('Y-m-d H:i:s'); + + $value = $serviceProvider->get('foo'); + assert($value === 'bar'); + } + + public function canInferTypeFromBuild(): void + { + $serviceProvider = $this->getServiceProvider(); + + $date = $serviceProvider->build(DateTimeImmutable::class); + echo $date->format('Y-m-d H:i:s'); + + $value = $serviceProvider->build('foo'); + assert($value === 'bar'); + } + + private function getServiceProvider(): ServiceLocatorInterface + { + $services = [ + 'foo' => 'bar', + DateTimeImmutable::class => new DateTimeImmutable(), + ]; + return new class ($services) implements ServiceLocatorInterface { + public function __construct(private readonly array $services) + { + } + + public function has(string $id): bool + { + return isset($this->services[$id]); + } + + public function build(string $name, ?array $options = null): mixed + { + /** @psalm-suppress MixedReturnStatement Yes indeed, can return mixed. */ + return $this->services[$name] ?? null; + } + + public function get(string $id): mixed + { + /** @psalm-suppress MixedReturnStatement Yes indeed, can return mixed. */ + return $this->services[$id] ?? null; + } + }; + } +} diff --git a/test/StaticAnalysis/SingleInstancePluginManager.php b/test/StaticAnalysis/SingleInstancePluginManager.php new file mode 100644 index 00000000..9f786544 --- /dev/null +++ b/test/StaticAnalysis/SingleInstancePluginManager.php @@ -0,0 +1,42 @@ + + */ +final class SingleInstancePluginManager extends AbstractSingleInstancePluginManager +{ + /** @var class-string */ + protected string $instanceOf = stdClass::class; + + public function getWhateverPlugin(array|null $options = null): stdClass + { + if ($options === null) { + return $this->get('foo'); + } + + return $this->build('foo', $options); + } + + public function functionValidateWhateverPlugin(object $object): stdClass + { + $this->validate($object); + return $object; + } + + public function getConcretePlugin(): ConcreteStdClassPlugin + { + return $this->get(ConcreteStdClassPlugin::class); + } + + public function buildConcretePlugin(): ConcreteStdClassPlugin + { + return $this->build(ConcreteStdClassPlugin::class); + } +} diff --git a/test/StaticAnalysis/UnionPluginManager.php b/test/StaticAnalysis/UnionPluginManager.php new file mode 100644 index 00000000..be5b0d15 --- /dev/null +++ b/test/StaticAnalysis/UnionPluginManager.php @@ -0,0 +1,58 @@ + + */ +final class UnionPluginManager extends AbstractPluginManager +{ + /** + * @return callable|CallableObjectType + */ + public function getWhateverPlugin(array|null $options = null): callable|object + { + if ($options === null) { + return $this->get('foo'); + } + + return $this->build('foo', $options); + } + + public function validateWhateverPlugin(mixed $plugin): callable|object + { + $this->validate($plugin); + return $plugin; + } + + public function getConcretePlugin(): ConcreteCallablePlugin + { + return self::get(ConcreteCallablePlugin::class); + } + + public function buildConcretePlugin(): ConcreteCallablePlugin + { + return self::build(ConcreteCallablePlugin::class); + } + + /** + * {@inheritDoc} + */ + public function validate(mixed $instance): void + { + if (! is_callable($instance)) { + throw new InvalidArgumentException('Provided instance is not callable.'); + } + } +} diff --git a/test/TestAsset/AbstractFactoryFoo.php b/test/TestAsset/AbstractFactoryFoo.php index 0dff8f8a..1b05c5f1 100644 --- a/test/TestAsset/AbstractFactoryFoo.php +++ b/test/TestAsset/AbstractFactoryFoo.php @@ -4,13 +4,13 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Psr\Container\ContainerInterface; final class AbstractFactoryFoo implements AbstractFactoryInterface { /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { if ($requestedName === 'foo') { return new Foo($options); @@ -19,10 +19,7 @@ public function __invoke(containerinterface $container, $requestedName, ?array $ return false; } - /** - * @param string $requestedName - */ - public function canCreate(containerinterface $container, $requestedName): bool + public function canCreate(ContainerInterface $container, string $requestedName): bool { return $requestedName === 'foo'; } diff --git a/test/TestAsset/CallTimesAbstractFactory.php b/test/TestAsset/CallTimesAbstractFactory.php index 5fce487e..4ec72463 100644 --- a/test/TestAsset/CallTimesAbstractFactory.php +++ b/test/TestAsset/CallTimesAbstractFactory.php @@ -4,15 +4,15 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Psr\Container\ContainerInterface; final class CallTimesAbstractFactory implements AbstractFactoryInterface { protected static int $callTimes = 0; /** {@inheritDoc} */ - public function canCreate(containerinterface $container, $name) + public function canCreate(ContainerInterface $container, string $name): bool { self::$callTimes++; @@ -20,7 +20,7 @@ public function canCreate(containerinterface $container, $name) } /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $className, ?array $options = null) + public function __invoke(ContainerInterface $container, string $className, ?array $options = null): mixed { } diff --git a/test/TestAsset/ClassWithCallbackMethod.php b/test/TestAsset/ClassWithCallbackMethod.php new file mode 100644 index 00000000..f4b05203 --- /dev/null +++ b/test/TestAsset/ClassWithCallbackMethod.php @@ -0,0 +1,18 @@ +callbackValue; + } +} 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 @@ + [ - InvokableObject::class => InvokableObject::class, - ], - 'delegators' => [ - 'foo' => [ - InvokableObject::class, - ], - ], - ]; -} diff --git a/test/TestAsset/FailingAbstractFactory.php b/test/TestAsset/FailingAbstractFactory.php index 81270ae9..6dbc74f8 100644 --- a/test/TestAsset/FailingAbstractFactory.php +++ b/test/TestAsset/FailingAbstractFactory.php @@ -4,19 +4,19 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Psr\Container\ContainerInterface; final class FailingAbstractFactory implements AbstractFactoryInterface { /** {@inheritDoc} */ - public function canCreate(containerinterface $container, $name) + public function canCreate(ContainerInterface $container, string $name): bool { return false; } /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $className, ?array $options = null) + public function __invoke(ContainerInterface $container, string $className, ?array $options = null): mixed { } } diff --git a/test/TestAsset/FailingExceptionWithStringAsCodeFactory.php b/test/TestAsset/FailingExceptionWithStringAsCodeFactory.php index 885506eb..bf79830d 100644 --- a/test/TestAsset/FailingExceptionWithStringAsCodeFactory.php +++ b/test/TestAsset/FailingExceptionWithStringAsCodeFactory.php @@ -4,13 +4,13 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; final class FailingExceptionWithStringAsCodeFactory implements FactoryInterface { /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { throw new ExceptionWithStringAsCodeException('There is an error'); } diff --git a/test/TestAsset/FailingFactory.php b/test/TestAsset/FailingFactory.php index 5c07e0b9..31b84c49 100644 --- a/test/TestAsset/FailingFactory.php +++ b/test/TestAsset/FailingFactory.php @@ -4,14 +4,14 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; use RuntimeException; final class FailingFactory implements FactoryInterface { /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null): mixed { throw new RuntimeException('There is an error'); } diff --git a/test/TestAsset/InvokableObjectPluginManager.php b/test/TestAsset/InvokableObjectPluginManager.php new file mode 100644 index 00000000..1e03e0e1 --- /dev/null +++ b/test/TestAsset/InvokableObjectPluginManager.php @@ -0,0 +1,37 @@ + + */ +final class InvokableObjectPluginManager extends AbstractSingleInstancePluginManager +{ + /** @var array */ + protected array $aliases = [ + 'foo' => InvokableObject::class, + + // v2 normalized FQCNs + 'laminastestservicemanagertestassetinvokableobject' => InvokableObject::class, + ]; + + /** @var FactoriesConfiguration */ + protected array $factories = [ + InvokableObject::class => InvokableFactory::class, + // Legacy (v2) due to alias resolution + 'laminastestservicemanagertestassetinvokableobject' => InvokableFactory::class, + ]; + + protected string $instanceOf = InvokableObject::class; + + protected bool $sharedByDefault = false; +} diff --git a/test/TestAsset/LenientPluginManager.php b/test/TestAsset/LenientPluginManager.php index d7ad8383..ff2c520a 100644 --- a/test/TestAsset/LenientPluginManager.php +++ b/test/TestAsset/LenientPluginManager.php @@ -5,21 +5,13 @@ namespace LaminasTest\ServiceManager\TestAsset; use Laminas\ServiceManager\AbstractPluginManager; -use Psr\Container\ContainerInterface; +/** + * @template-extends AbstractPluginManager + */ final class LenientPluginManager extends AbstractPluginManager { - /** - * Allow anything to be considered valid. - * - * @param mixed $instance - */ - public function validate($instance): void + public function validate(mixed $instance): void { } - - public function getCreationContext(): ContainerInterface - { - return $this->creationContext; - } } diff --git a/test/TestAsset/NonAutoInvokablePluginManager.php b/test/TestAsset/NonAutoInvokablePluginManager.php index 12303f42..e3e650b9 100644 --- a/test/TestAsset/NonAutoInvokablePluginManager.php +++ b/test/TestAsset/NonAutoInvokablePluginManager.php @@ -4,13 +4,11 @@ namespace LaminasTest\ServiceManager\TestAsset; -use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\AbstractSingleInstancePluginManager; -final class NonAutoInvokablePluginManager extends AbstractPluginManager +final class NonAutoInvokablePluginManager extends AbstractSingleInstancePluginManager { - /** @var bool */ - protected $autoAddInvokableClass = false; + protected bool $autoAddInvokableClass = false; - /** @var string */ - protected $instanceOf = InvokableObject::class; + protected string $instanceOf = InvokableObject::class; } diff --git a/test/TestAsset/PassthroughDelegatorFactory.php b/test/TestAsset/PassthroughDelegatorFactory.php index 8b807809..e1021a62 100644 --- a/test/TestAsset/PassthroughDelegatorFactory.php +++ b/test/TestAsset/PassthroughDelegatorFactory.php @@ -4,18 +4,20 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; +use Psr\Container\ContainerInterface; final class PassthroughDelegatorFactory implements DelegatorFactoryInterface { /** * {@inheritDoc} - * - * @see \Laminas\ServiceManager\Factory\DelegatorFactoryInterface::__invoke() */ - public function __invoke(containerinterface $container, $name, callable $callback, ?array $options = null) - { + public function __invoke( + ContainerInterface $container, + string $name, + callable $callback, + ?array $options = null + ): mixed { return $callback(); } } diff --git a/test/TestAsset/PreDelegator.php b/test/TestAsset/PreDelegator.php index 30d22129..ea7bedc9 100644 --- a/test/TestAsset/PreDelegator.php +++ b/test/TestAsset/PreDelegator.php @@ -4,14 +4,18 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; +use Psr\Container\ContainerInterface; final class PreDelegator implements DelegatorFactoryInterface { /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $name, callable $callback, ?array $options = null) - { + public function __invoke( + ContainerInterface $container, + string $name, + callable $callback, + ?array $options = null + ): mixed { if (! $container->has('config')) { return $callback(); } diff --git a/test/TestAsset/SampleFactory.php b/test/TestAsset/SampleFactory.php index cfc1124d..b57d83f5 100644 --- a/test/TestAsset/SampleFactory.php +++ b/test/TestAsset/SampleFactory.php @@ -4,17 +4,19 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerInterface; final class SampleFactory implements FactoryInterface { /** - * @param string $requestedName * @param array|null $options */ - public function __invoke(containerinterface $container, $requestedName, ?array $options = null): InvokableObject - { + public function __invoke( + ContainerInterface $container, + string $requestedName, + ?array $options = null + ): InvokableObject { return new InvokableObject(); } } diff --git a/test/TestAsset/SimpleAbstractFactory.php b/test/TestAsset/SimpleAbstractFactory.php index f126da44..32f820fb 100644 --- a/test/TestAsset/SimpleAbstractFactory.php +++ b/test/TestAsset/SimpleAbstractFactory.php @@ -4,19 +4,19 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Factory\AbstractFactoryInterface; +use Psr\Container\ContainerInterface; final class SimpleAbstractFactory implements AbstractFactoryInterface { /** {@inheritDoc} */ - public function canCreate(containerinterface $container, $name) + public function canCreate(ContainerInterface $container, string $name): bool { return true; } /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $className, ?array $options = null) + public function __invoke(ContainerInterface $container, string $className, ?array $options = null): mixed { if (empty($options)) { return new $className(); diff --git a/test/TestAsset/SimpleInitializer.php b/test/TestAsset/SimpleInitializer.php index 550a4ac4..4227d246 100644 --- a/test/TestAsset/SimpleInitializer.php +++ b/test/TestAsset/SimpleInitializer.php @@ -4,14 +4,14 @@ namespace LaminasTest\ServiceManager\TestAsset; -use interop\container\containerinterface; use Laminas\ServiceManager\Initializer\InitializerInterface; +use Psr\Container\ContainerInterface; use stdClass; final class SimpleInitializer implements InitializerInterface { /** {@inheritDoc} */ - public function __invoke(containerinterface $container, $instance) + public function __invoke(ContainerInterface $container, $instance) { if (! $instance instanceof stdClass) { return; diff --git a/test/TestAsset/SimplePluginManager.php b/test/TestAsset/SimplePluginManager.php index b0d8fa38..0962f9ef 100644 --- a/test/TestAsset/SimplePluginManager.php +++ b/test/TestAsset/SimplePluginManager.php @@ -4,10 +4,13 @@ namespace LaminasTest\ServiceManager\TestAsset; -use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\AbstractSingleInstancePluginManager; -final class SimplePluginManager extends AbstractPluginManager +/** + * @template-extends AbstractSingleInstancePluginManager + */ +final class SimplePluginManager extends AbstractSingleInstancePluginManager { - /** @var string */ - protected $instanceOf = InvokableObject::class; + /** @var class-string */ + protected string $instanceOf = InvokableObject::class; } diff --git a/test/TestAsset/SimpleServiceManager.php b/test/TestAsset/SimpleServiceManager.php index eada0ebd..c850f07e 100644 --- a/test/TestAsset/SimpleServiceManager.php +++ b/test/TestAsset/SimpleServiceManager.php @@ -11,7 +11,7 @@ final class SimpleServiceManager extends ServiceManager { /** @var array */ - protected $factories = [ + protected array $factories = [ stdClass::class => InvokableFactory::class, ]; } diff --git a/test/TestAsset/V2ValidationPluginManager.php b/test/TestAsset/V2ValidationPluginManager.php deleted file mode 100644 index 44a564fe..00000000 --- a/test/TestAsset/V2ValidationPluginManager.php +++ /dev/null @@ -1,32 +0,0 @@ -assertion)) { - throw new RuntimeException(sprintf( - '%s requires a callable $assertion property; not currently set', - self::class - )); - } - - ($this->assertion)($plugin); - } -} diff --git a/test/TestAsset/V2v3PluginManager.php b/test/TestAsset/V2v3PluginManager.php deleted file mode 100644 index 8c68462b..00000000 --- a/test/TestAsset/V2v3PluginManager.php +++ /dev/null @@ -1,74 +0,0 @@ - */ - protected $aliases = [ - 'foo' => InvokableObject::class, - - // Legacy Zend Framework aliases - \ZendTest\ServiceManager\TestAsset\InvokableObject::class => InvokableObject::class, - - // v2 normalized FQCNs - 'zendtestservicemanagertestassetinvokableobject' => InvokableObject::class, - ]; - - /** @var array */ - protected $factories = [ - InvokableObject::class => InvokableFactory::class, - // Legacy (v2) due to alias resolution - 'laminastestservicemanagertestassetinvokableobject' => InvokableFactory::class, - ]; - - /** @var string */ - protected $instanceOf = InvokableObject::class; - - /** @var bool */ - protected $shareByDefault = false; - - /** @var bool */ - protected $sharedByDefault = false; - - /** - * @param mixed $plugin - * @return void - * @throws InvalidServiceException - */ - public function validate($plugin) - { - if ($plugin instanceof $this->instanceOf) { - return; - } - - throw new InvalidServiceException(sprintf( - "'%s' is not an instance of '%s'", - $plugin::class, - $this->instanceOf - )); - } - - /** - * @param mixed $plugin - * @return void - * @throws RuntimeException - */ - public function validatePlugin($plugin) - { - try { - $this->validate($plugin); - } catch (InvalidServiceException $e) { - throw new RuntimeException($e->getMessage(), $e->getCode(), $e); - } - } -} diff --git a/test/TestAsset/factories/ComplexDependencyObject.php b/test/TestAsset/factories/ComplexDependencyObject.php index f80a8e31..c65d1ce7 100644 --- a/test/TestAsset/factories/ComplexDependencyObject.php +++ b/test/TestAsset/factories/ComplexDependencyObject.php @@ -4,19 +4,12 @@ namespace LaminasTest\ServiceManager\TestAsset; -use LaminasTest\ServiceManager\TestAsset\ComplexDependencyObject; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerInterface; class ComplexDependencyObjectFactory implements FactoryInterface { - /** - * @param ContainerInterface $container - * @param string $requestedName - * @param null|array $options - * @return ComplexDependencyObject - */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null): ComplexDependencyObject { return new ComplexDependencyObject( $container->get(\LaminasTest\ServiceManager\TestAsset\SimpleDependencyObject::class), diff --git a/test/TestAsset/factories/InvokableObject.php b/test/TestAsset/factories/InvokableObject.php index c5a96f64..328e4f2b 100644 --- a/test/TestAsset/factories/InvokableObject.php +++ b/test/TestAsset/factories/InvokableObject.php @@ -4,20 +4,13 @@ namespace LaminasTest\ServiceManager\TestAsset; -use LaminasTest\ServiceManager\TestAsset\InvokableObject; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerInterface; class InvokableObjectFactory implements FactoryInterface { - /** - * @param ContainerInterface $container - * @param string $requestedName - * @param null|array $options - * @return InvokableObject - */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null): InvokableObject { - return new InvokableObject(); + return new InvokableObject([]); } } diff --git a/test/TestAsset/factories/SimpleDependencyObject.php b/test/TestAsset/factories/SimpleDependencyObject.php index 26ed1fca..ce47f4fe 100644 --- a/test/TestAsset/factories/SimpleDependencyObject.php +++ b/test/TestAsset/factories/SimpleDependencyObject.php @@ -4,19 +4,12 @@ namespace LaminasTest\ServiceManager\TestAsset; -use LaminasTest\ServiceManager\TestAsset\SimpleDependencyObject; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerInterface; class SimpleDependencyObjectFactory implements FactoryInterface { - /** - * @param ContainerInterface $container - * @param string $requestedName - * @param null|array $options - * @return SimpleDependencyObject - */ - public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + public function __invoke(ContainerInterface $container, string $requestedName, array $options = null): SimpleDependencyObject { return new SimpleDependencyObject($container->get(\LaminasTest\ServiceManager\TestAsset\InvokableObject::class)); } diff --git a/test/TestAsset/laminas-code/ClassReflection.php b/test/TestAsset/laminas-code/ClassReflection.php deleted file mode 100644 index 7ad47694..00000000 --- a/test/TestAsset/laminas-code/ClassReflection.php +++ /dev/null @@ -1,231 +0,0 @@ -docBlock)) { - return $this->docBlock; - } - - if ('' == $this->getDocComment()) { - return false; - } - - $this->docBlock = new DocBlockReflection($this); - - return $this->docBlock; - } - - /** - * Return the start line of the class - * - * @param bool $includeDocComment - * @return int - */ - #[ReturnTypeWillChange] - public function getStartLine($includeDocComment = false) - { - if ($includeDocComment && $this->getDocComment() != '') { - return $this->getDocBlock()->getStartLine(); - } - - return parent::getStartLine(); - } - - /** - * Return the contents of the class - * - * @param bool $includeDocBlock - * @return string - */ - public function getContents($includeDocBlock = true) - { - $fileName = $this->getFileName(); - - if (false === $fileName || ! file_exists($fileName)) { - return ''; - } - - $filelines = file($fileName); - $startnum = $this->getStartLine($includeDocBlock); - $endnum = $this->getEndLine() - $this->getStartLine(); - - // Ensure we get between the open and close braces - $lines = array_slice($filelines, $startnum, $endnum); - array_unshift($lines, $filelines[$startnum - 1]); - - return strstr(implode('', $lines), '{'); - } - - /** - * Get all reflection objects of implemented interfaces - * - * @return ClassReflection[] - */ - #[ReturnTypeWillChange] - public function getInterfaces() - { - $phpReflections = parent::getInterfaces(); - $laminasReflections = []; - while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { - $instance = new ClassReflection($phpReflection->getName()); - $laminasReflections[] = $instance; - unset($phpReflection); - } - unset($phpReflections); - - return $laminasReflections; - } - - /** - * Return method reflection by name - * - * @param string $name - * @return MethodReflection - */ - #[ReturnTypeWillChange] - public function getMethod($name) - { - return new MethodReflection($this->getName(), parent::getMethod($name)->getName()); - } - - /** - * Get reflection objects of all methods - * - * @param int $filter - * @return MethodReflection[] - */ - #[ReturnTypeWillChange] - public function getMethods($filter = -1) - { - $methods = []; - foreach (parent::getMethods($filter) as $method) { - $instance = new MethodReflection($this->getName(), $method->getName()); - $methods[] = $instance; - } - - return $methods; - } - - /** - * Returns an array of reflection classes of traits used by this class. - * - * @return null|array - */ - #[ReturnTypeWillChange] - public function getTraits() - { - $vals = []; - $traits = parent::getTraits(); - if ($traits === null) { - return; - } - - foreach ($traits as $trait) { - $vals[] = new ClassReflection($trait->getName()); - } - - return $vals; - } - - /** - * Get parent reflection class of reflected class - * - * @return ClassReflection|bool - */ - #[ReturnTypeWillChange] - public function getParentClass() - { - $phpReflection = parent::getParentClass(); - if ($phpReflection) { - $laminasReflection = new ClassReflection($phpReflection->getName()); - unset($phpReflection); - - return $laminasReflection; - } - - return false; - } - - /** - * Return reflection property of this class by name - * - * @param string $name - * @return PropertyReflection - */ - #[ReturnTypeWillChange] - public function getProperty($name) - { - $phpReflection = parent::getProperty($name); - $laminasReflection = new PropertyReflection($this->getName(), $phpReflection->getName()); - unset($phpReflection); - - return $laminasReflection; - } - - /** - * Return reflection properties of this class - * - * @param int $filter - * @return PropertyReflection[] - */ - #[ReturnTypeWillChange] - public function getProperties($filter = -1) - { - $phpReflections = parent::getProperties($filter); - $laminasReflections = []; - while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { - $instance = new PropertyReflection($this->getName(), $phpReflection->getName()); - $laminasReflections[] = $instance; - unset($phpReflection); - } - unset($phpReflections); - - return $laminasReflections; - } - - /** - * @return string - */ - public function toString() - { - return parent::__toString(); - } - - /** - * @return string - */ - public function __toString() - { - return parent::__toString(); - } -} diff --git a/test/TestAsset/laminas-code/MethodReflection.php b/test/TestAsset/laminas-code/MethodReflection.php deleted file mode 100644 index 65d7fa50..00000000 --- a/test/TestAsset/laminas-code/MethodReflection.php +++ /dev/null @@ -1,466 +0,0 @@ -getDocComment()) { - return false; - } - - return new DocBlockReflection($this); - } - - /** - * Get start line (position) of method - * - * @param bool $includeDocComment - * @return int - */ - #[ReturnTypeWillChange] - public function getStartLine($includeDocComment = false) - { - if ($includeDocComment) { - if ($this->getDocComment() != '') { - return $this->getDocBlock()->getStartLine(); - } - } - - return parent::getStartLine(); - } - - /** - * Get reflection of declaring class - * - * @return ClassReflection - */ - #[ReturnTypeWillChange] - public function getDeclaringClass() - { - $phpReflection = parent::getDeclaringClass(); - $laminasReflection = new ClassReflection($phpReflection->getName()); - unset($phpReflection); - - return $laminasReflection; - } - - /** - * Get method prototype - * - * @param string $format - * @return array|string - */ - #[ReturnTypeWillChange] - public function getPrototype($format = self::PROTOTYPE_AS_ARRAY) - { - $returnType = 'mixed'; - $docBlock = $this->getDocBlock(); - if ($docBlock) { - $return = $docBlock->getTag('return'); - $returnTypes = $return->getTypes(); - $returnType = count($returnTypes) > 1 ? implode('|', $returnTypes) : $returnTypes[0]; - } - - $declaringClass = $this->getDeclaringClass(); - $prototype = [ - 'namespace' => $declaringClass->getNamespaceName(), - 'class' => substr($declaringClass->getName(), strlen($declaringClass->getNamespaceName()) + 1), - 'name' => $this->getName(), - 'visibility' => $this->isPublic() ? 'public' : ($this->isPrivate() ? 'private' : 'protected'), - 'return' => $returnType, - 'arguments' => [], - ]; - - $parameters = $this->getParameters(); - foreach ($parameters as $parameter) { - $prototype['arguments'][$parameter->getName()] = [ - 'type' => $parameter->detectType(), - 'required' => ! $parameter->isOptional(), - 'by_ref' => $parameter->isPassedByReference(), - 'default' => $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, - ]; - } - - if ($format == self::PROTOTYPE_AS_STRING) { - $line = $prototype['visibility'] . ' ' . $prototype['return'] . ' ' . $prototype['name'] . '('; - $args = []; - foreach ($prototype['arguments'] as $name => $argument) { - $argsLine = ($argument['type'] ? - $argument['type'] . ' ' - : '') . ($argument['by_ref'] ? '&' : '') . '$' . $name; - if (! $argument['required']) { - $argsLine .= ' = ' . var_export($argument['default'], true); - } - $args[] = $argsLine; - } - $line .= implode(', ', $args); - $line .= ')'; - - return $line; - } - - return $prototype; - } - - /** - * Get all method parameter reflection objects - * - * @return ParameterReflection[] - */ - #[ReturnTypeWillChange] - public function getParameters() - { - $phpReflections = parent::getParameters(); - $laminasReflections = []; - while ($phpReflections && ($phpReflection = array_shift($phpReflections))) { - $instance = new ParameterReflection( - [$this->getDeclaringClass()->getName(), $this->getName()], - $phpReflection->getName() - ); - $laminasReflections[] = $instance; - unset($phpReflection); - } - unset($phpReflections); - - return $laminasReflections; - } - - /** - * Get method contents - * - * @param bool $includeDocBlock - * @return string - */ - public function getContents($includeDocBlock = true) - { - $docComment = $this->getDocComment(); - $content = $includeDocBlock && ! empty($docComment) ? $docComment . "\n" : ''; - $content .= $this->extractMethodContents(); - - return $content; - } - - /** - * Get method body - * - * @return string - */ - public function getBody() - { - return $this->extractMethodContents(true); - } - - /** - * Tokenize method string and return concatenated body - * - * @param bool $bodyOnly - * @return string - */ - protected function extractMethodContents($bodyOnly = false) - { - $fileName = $this->getFileName(); - - if ((class_exists($this->class) && false === $fileName) || ! file_exists($fileName)) { - return ''; - } - - $lines = array_slice( - file($fileName, FILE_IGNORE_NEW_LINES), - $this->getStartLine() - 1, - $this->getEndLine() - ($this->getStartLine() - 1), - true - ); - - $functionLine = implode("\n", $lines); - $tokens = token_get_all(' $token) { - $tokenType = is_array($token) ? token_name($token[0]) : $token; - $tokenValue = is_array($token) ? $token[1] : $token; - - switch ($tokenType) { - case 'T_FINAL': - case 'T_ABSTRACT': - case 'T_PUBLIC': - case 'T_PROTECTED': - case 'T_PRIVATE': - case 'T_STATIC': - case 'T_FUNCTION': - // check to see if we have a valid function - // then check if we are inside function and have a closure - if ($this->isValidFunction($tokens, $key, $this->getName())) { - if ($bodyOnly === false) { - //if first instance of tokenType grab prefixed whitespace - //and append to body - if ($capture === false) { - $body .= $this->extractPrefixedWhitespace($tokens, $key); - } - $body .= $tokenValue; - } - - $capture = true; - } else { - //closure test - if ($firstBrace && $tokenType == 'T_FUNCTION') { - $body .= $tokenValue; - break; - } - $capture = false; - break; - } - break; - - case '{': - if ($capture === false) { - break; - } - - if ($firstBrace === false) { - $firstBrace = true; - if ($bodyOnly === true) { - break; - } - } - - $body .= $tokenValue; - break; - - case '}': - if ($capture === false) { - break; - } - - //check to see if this is the last brace - if ($this->isEndingBrace($tokens, $key)) { - //capture the end brace if not bodyOnly - if ($bodyOnly === false) { - $body .= $tokenValue; - } - - break 2; - } - - $body .= $tokenValue; - break; - - default: - if ($capture === false) { - break; - } - - // if returning body only wait for first brace before capturing - if ($bodyOnly === true && $firstBrace !== true) { - break; - } - - $body .= $tokenValue; - break; - } - } - - //remove ending whitespace and return - return rtrim($body); - } - - /** - * Take current position and find any whitespace - * - * @param array $haystack - * @param int $position - * @return string - */ - protected function extractPrefixedWhitespace($haystack, $position) - { - $content = ''; - $count = count($haystack); - if ($position + 1 == $count) { - return $content; - } - - for ($i = $position - 1; $i >= 0; $i--) { - $tokenType = is_array($haystack[$i]) ? token_name($haystack[$i][0]) : $haystack[$i]; - $tokenValue = is_array($haystack[$i]) ? $haystack[$i][1] : $haystack[$i]; - - //search only for whitespace - if ($tokenType == 'T_WHITESPACE') { - $content .= $tokenValue; - } else { - break; - } - } - - return $content; - } - - /** - * Test for ending brace - * - * @param array $haystack - * @param int $position - * @return bool - */ - protected function isEndingBrace($haystack, $position) - { - $count = count($haystack); - - //advance one position - $position += 1; - - if ($position == $count) { - return true; - } - - for ($i = $position; $i < $count; $i++) { - $tokenType = is_array($haystack[$i]) ? token_name($haystack[$i][0]) : $haystack[$i]; - switch ($tokenType) { - case 'T_FINAL': - case 'T_ABSTRACT': - case 'T_PUBLIC': - case 'T_PROTECTED': - case 'T_PRIVATE': - case 'T_STATIC': - return true; - - case 'T_FUNCTION': - // If a function is encountered and that function is not a closure - // then return true. otherwise the function is a closure, return false - if ($this->isValidFunction($haystack, $i)) { - return true; - } - return false; - - case '}': - case ';': - case 'T_BREAK': - case 'T_CATCH': - case 'T_DO': - case 'T_ECHO': - case 'T_ELSE': - case 'T_ELSEIF': - case 'T_EVAL': - case 'T_EXIT': - case 'T_FINALLY': - case 'T_FOR': - case 'T_FOREACH': - case 'T_GOTO': - case 'T_IF': - case 'T_INCLUDE': - case 'T_INCLUDE_ONCE': - case 'T_PRINT': - case 'T_STRING': - case 'T_STRING_VARNAME': - case 'T_THROW': - case 'T_USE': - case 'T_VARIABLE': - case 'T_WHILE': - case 'T_YIELD': - return false; - } - } - } - - /** - * Test to see if current position is valid function or - * closure. Returns true if it's a function and NOT a closure - * - * @param array $haystack - * @param int $position - * @param string $functionName - * @return bool - */ - protected function isValidFunction($haystack, $position, $functionName = null) - { - $isValid = false; - $count = count($haystack); - for ($i = $position + 1; $i < $count; $i++) { - $tokenType = is_array($haystack[$i]) ? token_name($haystack[$i][0]) : $haystack[$i]; - $tokenValue = is_array($haystack[$i]) ? $haystack[$i][1] : $haystack[$i]; - - //check for occurrence of ( or - if ($tokenType == 'T_STRING') { - //check to see if function name is passed, if so validate against that - if ($functionName !== null && $tokenValue != $functionName) { - $isValid = false; - break; - } - - $isValid = true; - break; - } elseif ($tokenValue == '(') { - break; - } - } - - return $isValid; - } - - /** - * @return string - */ - public function toString() - { - return parent::__toString(); - } - - /** - * @return string - */ - public function __toString() - { - return parent::__toString(); - } -} diff --git a/test/TestAsset/laminas-code/ParameterReflection.php b/test/TestAsset/laminas-code/ParameterReflection.php deleted file mode 100644 index d1ba559d..00000000 --- a/test/TestAsset/laminas-code/ParameterReflection.php +++ /dev/null @@ -1,129 +0,0 @@ -getName()); - unset($phpReflection); - - return $laminasReflection; - } - - /** - * Get class reflection object - * - * @return null|ClassReflection - */ - #[ReturnTypeWillChange] - public function getClass() - { - $phpReflectionType = parent::getType(); - if ($phpReflectionType === null) { - return null; - } - - $laminasReflection = new ClassReflection($phpReflectionType->getName()); - unset($phpReflectionType); - - return $laminasReflection; - } - - /** - * Get declaring function reflection object - * - * @return FunctionReflection|MethodReflection - */ - #[ReturnTypeWillChange] - public function getDeclaringFunction() - { - $phpReflection = parent::getDeclaringFunction(); - if ($phpReflection instanceof ReflectionMethod) { - $laminasReflection = new MethodReflection($this->getDeclaringClass()->getName(), $phpReflection->getName()); - } else { - $laminasReflection = new FunctionReflection($phpReflection->getName()); - } - unset($phpReflection); - - return $laminasReflection; - } - - /** - * Get parameter type - * - * @return string|null - */ - public function detectType() - { - if ( - method_exists($this, 'getType') - && null !== ($type = $this->getType()) - && $type->isBuiltin() - ) { - return $type->getName(); - } - - if (null !== $type && $type->getName() === 'self') { - return $this->getDeclaringClass()->getName(); - } - - if (($class = $this->getClass()) instanceof ReflectionClass) { - return $class->getName(); - } - - $docBlock = $this->getDeclaringFunction()->getDocBlock(); - - if (! $docBlock instanceof DocBlockReflection) { - return null; - } - - $params = $docBlock->getTags('param'); - - if (isset($params[$this->getPosition()])) { - return $params[$this->getPosition()]->getType(); - } - - return null; - } - - /** - * @return string - */ - public function toString() - { - return parent::__toString(); - } - - /** - * @return string - */ - public function __toString() - { - return parent::__toString(); - } -} diff --git a/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php new file mode 100644 index 00000000..e8f5427f --- /dev/null +++ b/test/Tool/AheadOfTimeFactoryCompiler/AheadOfTimeFactoryCompilerTest.php @@ -0,0 +1,254 @@ +factoryCreator = $this->createMock(FactoryCreatorInterface::class); + $this->compiler = new AheadOfTimeFactoryCompiler( + $this->factoryCreator, + ); + } + + /** + * @return array + */ + public static 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($config)); + } + + 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 static function nonClassReferencingServiceNames(): array + { + return [ + 'nonexistent-service-name' => [ + 'foobar', + ], + 'interface' => [ + FactoryInterface::class, + ], + 'trait' => [ + WhateverTrait::class, + ], + ]; + } + + /** + * @return array + */ + public static 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); + } + + 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 new file mode 100644 index 00000000..6f0bffa3 --- /dev/null +++ b/test/Tool/AheadOfTimeFactoryCompiler/TestAsset/WhateverEnum.php @@ -0,0 +1,10 @@ +configDir = vfsStream::setup('project'); - $this->helper = $this->createMock(ConsoleHelper::class); - $this->command = new ConfigDumperCommand(ConfigDumperCommand::class, $this->helper); - } - - /** - * @param resource $stream - */ - public function assertHelp($stream = STDOUT): void - { - $this->helper - ->expects(self::once()) - ->method('writeLine') - ->with( - self::stringContains('Usage:'), - true, - $stream - ); - } - - public function assertErrorRaised(string $message): void - { - $this->helper - ->expects(self::once()) - ->method('writeErrorMessage') - ->with(self::stringContains($message)); - } - - public function testEmitsHelpWhenNoArgumentsProvided(): void - { - $this->assertHelp(); - self::assertEquals(0, $this->command->__invoke([])); - } - - public static function helpArguments(): array - { - return [ - 'short' => ['-h'], - 'long' => ['--help'], - 'literal' => ['help'], - ]; - } - - public static function ignoreUnresolvedArguments(): array - { - return [ - 'short' => ['-i'], - 'long' => ['--ignore-unresolved'], - ]; - } - - /** - * @dataProvider helpArguments - */ - public function testEmitsHelpWhenHelpArgumentProvidedAsFirstArgument(string $argument): void - { - $this->assertHelp(); - self::assertEquals(0, $this->command->__invoke([$argument])); - } - - public function testEmitsErrorWhenTooFewArgumentsPresent(): void - { - $this->assertErrorRaised('Missing class name'); - $this->assertHelp(STDERR); - self::assertEquals(1, $this->command->__invoke(['foo'])); - } - - public function testRaisesExceptionIfConfigFileNotFoundAndDirectoryNotWritable(): void - { - vfsStream::newDirectory('config', 0550) - ->at($this->configDir); - $config = vfsStream::url('project/config/test.config.php'); - - $this->assertErrorRaised(sprintf('Cannot create configuration at path "%s"; not writable.', $config)); - $this->assertHelp(STDERR); - self::assertEquals(1, $this->command->__invoke([$config, 'Not\A\Real\Class'])); - } - - public function testGeneratesConfigFileWhenProvidedConfigurationFileNotFound(): void - { - vfsStream::newDirectory('config', 0775) - ->at($this->configDir); - $config = vfsStream::url('project/config/test.config.php'); - - $this->helper - ->expects(self::once()) - ->method('writeLine') - ->with('[DONE] Changes written to ' . $config); - - self::assertEquals(0, $this->command->__invoke([$config, SimpleDependencyObject::class])); - - $generated = include $config; - - self::assertIsArray($generated); - self::assertArrayHasKey(ConfigAbstractFactory::class, $generated); - - $factoryConfig = $generated[ConfigAbstractFactory::class]; - - self::assertIsArray($factoryConfig); - self::assertArrayHasKey(SimpleDependencyObject::class, $factoryConfig); - self::assertArrayHasKey(InvokableObject::class, $factoryConfig); - self::assertContains(InvokableObject::class, $factoryConfig[SimpleDependencyObject::class]); - self::assertEquals([], $factoryConfig[InvokableObject::class]); - } - - /** - * @dataProvider ignoreUnresolvedArguments - */ - public function testGeneratesConfigFileIgnoringUnresolved(string $argument): void - { - vfsStream::newDirectory('config', 0775) - ->at($this->configDir); - $config = vfsStream::url('project/config/test.config.php'); - - $this->helper - ->expects(self::once()) - ->method('writeLine') - ->with('[DONE] Changes written to ' . $config); - - self::assertEquals(0, $this->command->__invoke([$argument, $config, ObjectWithObjectScalarDependency::class])); - - $generated = include $config; - - self::assertIsArray($generated); - self::assertArrayHasKey(ConfigAbstractFactory::class, $generated); - - $factoryConfig = $generated[ConfigAbstractFactory::class]; - - self::assertIsArray($factoryConfig); - self::assertArrayHasKey(SimpleDependencyObject::class, $factoryConfig); - self::assertArrayHasKey(InvokableObject::class, $factoryConfig); - self::assertContains(InvokableObject::class, $factoryConfig[SimpleDependencyObject::class]); - self::assertEquals([], $factoryConfig[InvokableObject::class]); - - self::assertArrayHasKey(ObjectWithObjectScalarDependency::class, $factoryConfig); - self::assertContains( - SimpleDependencyObject::class, - $factoryConfig[ObjectWithObjectScalarDependency::class] - ); - self::assertContains( - ObjectWithScalarDependency::class, - $factoryConfig[ObjectWithObjectScalarDependency::class] - ); - } - - public function testEmitsErrorWhenConfigurationFileDoesNotReturnArray(): void - { - vfsStream::newFile('config/invalid.config.php') - ->at($this->configDir) - ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/invalid.config.php'))); - $config = vfsStream::url('project/config/invalid.config.php'); - - $this->assertErrorRaised('Configuration at path "' . $config . '" does not return an array.'); - $this->assertHelp(STDERR); - self::assertEquals(1, $this->command->__invoke([$config, 'Not\A\Real\Class'])); - } - - public function testEmitsErrorWhenClassDoesNotExist(): void - { - vfsStream::newFile('config/test.config.php') - ->at($this->configDir) - ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/test.config.php'))); - $config = vfsStream::url('project/config/test.config.php'); - - $this->assertErrorRaised('Class "Not\\A\\Real\\Class" does not exist or could not be autoloaded.'); - $this->assertHelp(STDERR); - self::assertEquals(1, $this->command->__invoke([$config, 'Not\A\Real\Class'])); - } - - public function testEmitsErrorWhenUnableToCreateConfiguration(): void - { - vfsStream::newFile('config/test.config.php') - ->at($this->configDir) - ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/test.config.php'))); - $config = vfsStream::url('project/config/test.config.php'); - - $this->assertErrorRaised('Unable to create config for "' . ObjectWithScalarDependency::class . '":'); - $this->assertHelp(STDERR); - self::assertEquals(1, $this->command->__invoke([$config, ObjectWithScalarDependency::class])); - } - - public function testEmitsConfigFileToStdoutWhenSuccessful(): void - { - vfsStream::newFile('config/test.config.php') - ->at($this->configDir) - ->setContent(file_get_contents(realpath(__DIR__ . '/../TestAsset/config/test.config.php'))); - $config = vfsStream::url('project/config/test.config.php'); - - $this->helper - ->expects(self::once()) - ->method('writeLine') - ->with('[DONE] Changes written to ' . $config); - - self::assertEquals(0, $this->command->__invoke([$config, SimpleDependencyObject::class])); - - $generated = include $config; - - self::assertIsArray($generated); - self::assertArrayHasKey(ConfigAbstractFactory::class, $generated); - - $factoryConfig = $generated[ConfigAbstractFactory::class]; - - self::assertIsArray($factoryConfig); - self::assertArrayHasKey(SimpleDependencyObject::class, $factoryConfig); - self::assertArrayHasKey(InvokableObject::class, $factoryConfig); - self::assertContains(InvokableObject::class, $factoryConfig[SimpleDependencyObject::class]); - self::assertEquals([], $factoryConfig[InvokableObject::class]); - } -} diff --git a/test/Tool/ConfigDumperTest.php b/test/Tool/ConfigDumperTest.php index 8372fcac..3ec0da15 100644 --- a/test/Tool/ConfigDumperTest.php +++ b/test/Tool/ConfigDumperTest.php @@ -4,7 +4,6 @@ namespace LaminasTest\ServiceManager\Tool; -use interop\container\containerinterface; use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory; use Laminas\ServiceManager\Exception\InvalidArgumentException; use Laminas\ServiceManager\Factory\FactoryInterface; @@ -18,6 +17,7 @@ use LaminasTest\ServiceManager\TestAsset\SecondComplexDependencyObject; use LaminasTest\ServiceManager\TestAsset\SimpleDependencyObject; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use function file_put_contents; use function sys_get_temp_dir; @@ -38,25 +38,6 @@ protected function setUp(): void $this->dumper = new ConfigDumper(); } - public function testCreateDependencyConfigExceptsIfClassNameIsNotString(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class name must be a string, integer given'); - - /** @psalm-suppress InvalidArgument */ - $this->dumper->createDependencyConfig([], 42); - } - - public function testCreateDependencyConfigExceptsIfClassDoesNotExist(): void - { - $className = 'Dirk\Gentley\Holistic\Detective\Agency'; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot find class or interface with name ' . $className); - - $this->dumper->createDependencyConfig([], $className); - } - public function testCreateDependencyConfigInvokableObjectReturnsEmptyArray(): void { $config = $this->dumper->createDependencyConfig([], InvokableObject::class); @@ -127,7 +108,7 @@ public function testCreateDependencyConfigWithContainerAndNoServiceWithoutTypeHi . 'it has no type hint, or non-class/interface type hint' ); - $container = $this->createMock(containerinterface::class); + $container = $this->createMock(ContainerInterface::class); $container ->expects(self::once()) ->method('has') @@ -144,7 +125,7 @@ public function testCreateDependencyConfigWithContainerAndNoServiceWithoutTypeHi public function testCreateDependencyConfigWithContainerWithoutTypeHintedParameter(): void { - $container = $this->createMock(containerinterface::class); + $container = $this->createMock(ContainerInterface::class); $container ->expects(self::once()) ->method('has') @@ -229,25 +210,6 @@ public function testCreateDependencyConfigWorksWithMultipleDependenciesOfSameTyp self::assertSame($expectedConfig, $this->dumper->createDependencyConfig([], DoubleDependencyObject::class)); } - public function testCreateFactoryMappingsExceptsIfClassNameIsNotString(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class name must be a string, integer given'); - - /** @psalm-suppress InvalidArgument */ - $this->dumper->createFactoryMappings([], 42); - } - - public function testCreateFactoryMappingsExceptsIfClassDoesNotExist(): void - { - $className = 'Dirk\Gentley\Holistic\Detective\Agency'; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Cannot find class or interface with name ' . $className); - - $this->dumper->createFactoryMappings([], $className); - } - public function testCreateFactoryMappingsReturnsUnmodifiedArrayIfMappingExists(): void { $config = [ diff --git a/test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php b/test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php new file mode 100644 index 00000000..2974bbd6 --- /dev/null +++ b/test/Tool/ConstructorParameterResolver/ConstructorParameterResolverTest.php @@ -0,0 +1,306 @@ +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') + ->willReturnMap([ + ['config', false], + [SampleInterface::class, 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.'); + } +} diff --git a/test/Tool/FactoryCreatorCommandTest.php b/test/Tool/FactoryCreatorCommandTest.php deleted file mode 100644 index e2ac905c..00000000 --- a/test/Tool/FactoryCreatorCommandTest.php +++ /dev/null @@ -1,122 +0,0 @@ -helper = $this->createMock(ConsoleHelper::class); - $this->command = new FactoryCreatorCommand(ConfigDumperCommand::class, $this->helper); - } - - public function testEmitsHelpWhenNoArgumentsProvided(): void - { - $this->assertHelp(); - self::assertSame(0, $this->command->__invoke([])); - } - - /** - * @param resource $stream - */ - public function assertHelp($stream = STDOUT): void - { - $this->helper - ->expects(self::once()) - ->method('writeLine') - ->with( - self::stringContains('Usage:'), - true, - $stream - ); - } - - public static function helpArguments(): array - { - return [ - 'short' => ['-h'], - 'long' => ['--help'], - 'literal' => ['help'], - ]; - } - - /** - * @dataProvider helpArguments - */ - public function testEmitsHelpWhenHelpArgumentProvidedAsFirstArgument(string $argument): void - { - $this->assertHelp(); - self::assertSame(0, $this->command->__invoke([$argument])); - } - - public static function invalidArguments(): array - { - return [ - 'string' => ['string'], - 'interface' => [FactoryInterface::class], - ]; - } - - /** - * @dataProvider invalidArguments - */ - public function testEmitsErrorMessageIfArgumentIsNotAClass(string $argument): void - { - $this->assertErrorRaised(sprintf('Class "%s" does not exist', $argument)); - $this->assertHelp(STDERR); - self::assertSame(1, $this->command->__invoke([$argument])); - } - - public function assertErrorRaised(string $message): void - { - $this->helper - ->expects(self::once()) - ->method('writeErrorMessage') - ->with(self::stringContains($message)); - } - - public function testEmitsErrorWhenUnableToCreateFactory(): void - { - $this->assertErrorRaised('Unable to create factory for "' . ObjectWithScalarDependency::class . '":'); - $this->assertHelp(STDERR); - self::assertSame(1, $this->command->__invoke([ObjectWithScalarDependency::class])); - } - - public function testEmitsFactoryFileToStdoutWhenSuccessful(): void - { - $expected = file_get_contents(__DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'); - - $this->helper - ->expects(self::once()) - ->method('write') - ->with($expected, false); - - $this->assertSame(0, $this->command->__invoke([SimpleDependencyObject::class])); - } -} diff --git a/test/Tool/FactoryCreatorTest.php b/test/Tool/FactoryCreatorTest.php index f9c56d0e..2efde2b6 100644 --- a/test/Tool/FactoryCreatorTest.php +++ b/test/Tool/FactoryCreatorTest.php @@ -4,17 +4,27 @@ namespace LaminasTest\ServiceManager\Tool; +use Laminas\ServiceManager\Tool\ConstructorParameterResolver\ConstructorParameterResolver; use Laminas\ServiceManager\Tool\FactoryCreator; use LaminasTest\ServiceManager\TestAsset\ComplexDependencyObject; -use LaminasTest\ServiceManager\TestAsset\Foo; +use LaminasTest\ServiceManager\TestAsset\DelegatorAndAliasBehaviorTest\TargetObjectDelegator; use LaminasTest\ServiceManager\TestAsset\InvokableObject; +use LaminasTest\ServiceManager\TestAsset\SecondComplexDependencyObject; use LaminasTest\ServiceManager\TestAsset\SimpleDependencyObject; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use stdClass; -use function class_alias; +use function array_pop; +use function count; use function file_get_contents; +use function func_get_args; +use function is_array; use function preg_match; +use const PHP_EOL; + /** * @covers \Laminas\ServiceManager\Tool\FactoryCreator */ @@ -22,14 +32,18 @@ final class FactoryCreatorTest extends TestCase { private FactoryCreator $factoryCreator; - /** - * @internal param FactoryCreator $factoryCreator - */ + /** @var MockObject&ContainerInterface */ + private ContainerInterface $container; + protected function setUp(): void { parent::setUp(); - $this->factoryCreator = new FactoryCreator(); + $this->container = $this->createMock(ContainerInterface::class); + $this->factoryCreator = new FactoryCreator( + $this->container, + new ConstructorParameterResolver(), + ); } public function testCreateFactoryCreatesForInvokable(): void @@ -44,6 +58,12 @@ public function testCreateFactoryCreatesForSimpleDependencies(): void { $className = SimpleDependencyObject::class; $factory = file_get_contents(__DIR__ . '/../TestAsset/factories/SimpleDependencyObject.php'); + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnCallback($this->createReturnMapCallbackWithDefault([ + [InvokableObject::class, true], + ], false)); self::assertSame($factory, $this->factoryCreator->createFactory($className)); } @@ -53,22 +73,68 @@ public function testCreateFactoryCreatesForComplexDependencies(): void $className = ComplexDependencyObject::class; $factory = file_get_contents(__DIR__ . '/../TestAsset/factories/ComplexDependencyObject.php'); + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnCallback($this->createReturnMapCallbackWithDefault([ + [SimpleDependencyObject::class, true], + [SecondComplexDependencyObject::class, true], + ], false)); + self::assertSame($factory, $this->factoryCreator->createFactory($className)); } public function testNamespaceGeneration(): void { $testClassNames = [ - 'Foo\\Bar\\Service' => 'Foo\\Bar', - 'Foo\\Service\\Bar\\Service' => 'Foo\\Service\\Bar', + ComplexDependencyObject::class => 'LaminasTest\\ServiceManager\\TestAsset', + TargetObjectDelegator::class => 'LaminasTest\\ServiceManager\\TestAsset\\DelegatorAndAliasBehaviorTest', + stdClass::class => '', ]; + + $this->container + ->expects(self::atLeastOnce()) + ->method('has') + ->willReturnCallback($this->createReturnMapCallbackWithDefault([ + [SimpleDependencyObject::class, true], + [SecondComplexDependencyObject::class, true], + ], false)); + foreach ($testClassNames as $testFqcn => $expectedNamespace) { - class_alias(Foo::class, $testFqcn); $generatedFactory = $this->factoryCreator->createFactory($testFqcn); + + if ($expectedNamespace === '') { + self::assertStringNotContainsString(PHP_EOL . 'namespace ', $generatedFactory); + continue; + } + preg_match('/^namespace\s([^;]+)/m', $generatedFactory, $namespaceMatch); self::assertNotEmpty($namespaceMatch); self::assertSame($expectedNamespace, $namespaceMatch[1]); } } + + private function createReturnMapCallbackWithDefault(array $values, mixed $default): callable + { + return function () use ($values, $default): mixed { + $args = func_get_args(); + $parameterCount = count($args); + + foreach ($values as $map) { + if (! is_array($map) || $parameterCount !== count($map) - 1) { + continue; + } + + /** @var mixed $return */ + $return = array_pop($map); + + if ($args === $map) { + return $return; + } + } + + return $default; + }; + } } diff --git a/test/autoload.php b/test/autoload.php deleted file mode 100644 index 4513c844..00000000 --- a/test/autoload.php +++ /dev/null @@ -1,5 +0,0 @@ -