diff --git a/.changelog/framework-composition.md b/.changelog/framework-composition.md new file mode 100644 index 0000000..d188797 --- /dev/null +++ b/.changelog/framework-composition.md @@ -0,0 +1,7 @@ +--- +category: changed +type: minor +--- + +Composes the attributed invocable commands through the published Fast Forward +Container autowiring API, with focused provider bindings and explicit overrides. diff --git a/README.md b/README.md index 945c3a2..aa98c3c 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ composer test `check` reports violations with a nonzero status and does not change source. `fix` explicitly applies ECS fixes. It does not commit, push or update dependencies. Recheck and inspect the diff afterward; failures can leave already applied fixes. -No selected PHP files is a successful no-op. Invalid options, configuration, Git -history or head mismatch fail closed with exit 2. Otherwise tool exit status is -propagated. Changed mode requires a clean tracked checkout of the requested head. +An empty PHP selection is a successful no-op. Invalid options, configuration paths, +Git history or head mismatch fail closed with exit 2. Syntax/style tool failures, +including executable configuration errors inside ECS, retain their tool status. +Changed mode requires a clean tracked checkout of the requested head. `--all` includes tracked files and permits inspecting local tracked edits. Symfony Console provides `help check`, standard global flags and both `--option=value` and `--option value`. Error diagnostics use stderr. Empty paths, diff --git a/bin/code-standards b/bin/code-standards index 1baaa3c..aa90dc1 100644 --- a/bin/code-standards +++ b/bin/code-standards @@ -4,30 +4,14 @@ declare(strict_types=1); use FastForward\CodeStandards\Console\Application; -use FastForward\CodeStandards\Console\Command\CheckCommand; -use FastForward\CodeStandards\Console\Command\FixCommand; -use FastForward\CodeStandards\Console\StandardsOperation; -use FastForward\CodeStandards\Filesystem\PhpFiles; -use FastForward\CodeStandards\Git\FileSelection; -use FastForward\CodeStandards\Process\NativeProcessRunner; -use FastForward\CodeStandards\Project\ProjectScopeResolver; -use FastForward\CodeStandards\Standards\StandardsRunner; +use FastForward\CodeStandards\DependencyInjection\StandardsServiceProvider; +use FastForward\Container\ContainerFactory; use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Process\Process; $autoload = $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; require $autoload; -$vendor = dirname($autoload); $output = new ConsoleOutput(); -$process = new NativeProcessRunner(static function (string $type, string $message) use ($output): void { - $stream = Process::ERR === $type ? $output->getErrorOutput() : $output; - $stream->write($message); -}); -$projects = new ProjectScopeResolver(dirname(__DIR__) . '/config/ecs.php'); -$selection = new FileSelection($process); -$files = new PhpFiles(); -$standards = new StandardsRunner($process, $vendor . '/symplify/easy-coding-standard/bin/ecs'); -$operation = new StandardsOperation($projects, $selection, $files, $standards); +$container = ContainerFactory::create(new StandardsServiceProvider(dirname($autoload), $output)); -exit((new Application(new CheckCommand($operation), new FixCommand($operation)))->run(output: $output)); +exit($container->get(Application::class)->run(output: $output)); diff --git a/composer.json b/composer.json index 58711ac..6d7f276 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "license": "MIT", "require": { "php": "^8.3", + "fast-forward/container": "~1.10.1", "symfony/console": "^7.4 || ^8.0", "symfony/process": "^7.4 || ^8.0", "symplify/easy-coding-standard": "^13.0" diff --git a/composer.lock b/composer.lock index f42db87..084cd1f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,422 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dad04c1e4b1938500b5b659cd9bd9c29", + "content-hash": "53fa33641f1aadb904734255031fbd70", "packages": [ + { + "name": "brick/varexporter", + "version": "0.7.0", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "b3a50b8f630a9ed5015ea3e1f00479af261ed80d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/b3a50b8f630a9ed5015ea3e1f00479af261ed80d", + "reference": "b3a50b8f630a9ed5015ea3e1f00479af261ed80d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^11.0", + "vimeo/psalm": "6.14.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.7.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-01-06T22:56:00+00:00" + }, + { + "name": "container-interop/service-provider", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/container-interop/service-provider.git", + "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/service-provider/zipball/e04441ca21ef03e10dce70b0af29269281eec6dc", + "reference": "e04441ca21ef03e10dce70b0af29269281eec6dc", + "shasum": "" + }, + "require": { + "psr/container": "^1.0 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting container interoperability through standard service providers", + "homepage": "https://github.com/container-interop/service-provider", + "support": { + "issues": "https://github.com/container-interop/service-provider/issues", + "source": "https://github.com/container-interop/service-provider/tree/v0.4.1" + }, + "time": "2023-12-14T14:50:12+00:00" + }, + { + "name": "fast-forward/config", + "version": "v1.7.3", + "source": { + "type": "git", + "url": "https://github.com/php-fast-forward/config.git", + "reference": "2b5ade2cc6a7141c0e7906a555978b9bfafa1edd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fast-forward/config/zipball/2b5ade2cc6a7141c0e7906a555978b9bfafa1edd", + "reference": "2b5ade2cc6a7141c0e7906a555978b9bfafa1edd", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.5 || ^0.6 || ^0.7", + "laminas/laminas-config-aggregator": "^1.13", + "php": "^8.3", + "psr/container": "^1.0 || ^2.0", + "psr/simple-cache": "^3.0" + }, + "require-dev": { + "fast-forward/dev-tools": "dev-main", + "symfony/cache": "^7.4" + }, + "suggest": { + "llm/skills": "Optionally synchronize the bundled agent skill in consumer projects." + }, + "type": "library", + "extra": { + "skills": { + "source": "resources/skills" + }, + "grumphp": { + "config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml" + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastForward\\Config\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Felipe Sayão Lobato Abreu", + "email": "github@mentordosnerds.com" + } + ], + "description": "Fast Forward Config utility classes", + "homepage": "https://php-fast-forward.github.io/config/", + "support": { + "issues": "https://github.com/php-fast-forward/config/issues", + "source": "https://github.com/php-fast-forward/config" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate/?business=JLDAF45XZ8D84", + "type": "custom" + }, + { + "url": "https://github.com/sponsors/php-fast-forward", + "type": "github" + } + ], + "time": "2026-10-20T15:00:07+00:00" + }, + { + "name": "fast-forward/container", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/php-fast-forward/container.git", + "reference": "a75dda2c8a9bd9cb2ef5bb25abc1a6b09de8a298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fast-forward/container/zipball/a75dda2c8a9bd9cb2ef5bb25abc1a6b09de8a298", + "reference": "a75dda2c8a9bd9cb2ef5bb25abc1a6b09de8a298", + "shasum": "" + }, + "require": { + "container-interop/service-provider": "^0.4.1", + "fast-forward/config": "^1.6", + "php": "^8.3", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "fast-forward/dev-tools": "dev-main" + }, + "suggest": { + "llm/skills": "Automatically synchronize the bundled agent skill in consumer projects." + }, + "type": "library", + "extra": { + "skills": { + "source": "resources/skills" + }, + "grumphp": { + "config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml" + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastForward\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Felipe Sayão Lobato Abreu", + "email": "github@mentordosnerds.com" + } + ], + "description": "Fast Forward PSR-11 Container utility classes", + "homepage": "https://php-fast-forward.github.io/container/", + "support": { + "issues": "https://github.com/php-fast-forward/container/issues", + "source": "https://github.com/php-fast-forward/container" + }, + "funding": [ + { + "url": "https://github.com/php-fast-forward", + "type": "github" + } + ], + "time": "2026-10-18T08:00:17+00:00" + }, + { + "name": "laminas/laminas-config-aggregator", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config-aggregator.git", + "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/75331b17a9ba72c5fa8679988ef08a588ddd86af", + "reference": "75331b17a9ba72c5fa8679988ef08a588ddd86af", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.5.0 || ^0.4.0 || ^0.6.0 || ^0.7.0", + "laminas/laminas-stdlib": "^3.18.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "webimpress/safe-writer": "^2.2.0" + }, + "conflict": { + "nikic/php-parser": "<4.12", + "zendframework/zend-config-aggregator": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-config": "^3.10.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", + "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", + "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ConfigAggregator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Lightweight library for collecting and merging configuration from different sources", + "homepage": "https://laminas.dev", + "keywords": [ + "config-aggregator", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config-aggregator/issues", + "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", + "source": "https://github.com/laminas/laminas-config-aggregator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2026-06-01T15:37:55+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.21.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b1c81514cfe158aadf724c42b34d3d0a8164c096", + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-stdlib": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^3.1.0", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T18:13:12+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-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/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -59,6 +473,57 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, { "name": "symfony/console", "version": "v7.4.21", @@ -853,6 +1318,65 @@ "source": "https://github.com/ecsphp/ecs/tree/13.3.8" }, "time": "2026-10-20T21:02:34+00:00" + }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" } ], "packages-dev": [ @@ -916,63 +1440,6 @@ ], "time": "2026-08-11T10:17:44+00:00" }, - { - "name": "nikic/php-parser", - "version": "v5.9.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", - "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-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/v5.9.0" - }, - "time": "2026-09-13T18:51:52+00:00" - }, { "name": "phar-io/manifest", "version": "2.0.4", diff --git a/docs/architecture.md b/docs/architecture.md index 383c36a..9871f0f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -60,8 +60,14 @@ Code Standards follows that separation with Symfony Console 7.4-compatible APIs, matching this project's PHP 8.3 minimum. Symfony owns argument binding, help and global options. Commands are plain invocable PHP objects with `AsCommand` and mapped `Option` attributes, adapted by native `Application::addCommand()`; they -do not extend Symfony Command. These APIs are present in published Console7.4. +do not extend Symfony Command. These APIs are present in published Console 7.4. There is no custom parser or speculative lazy-command layer. +Fast Forward Container autowires the concrete application, commands, scan +orchestration, file selector and PHP filter. The module's service provider binds +only the process/output interfaces and services needing installation/config +paths. The binary supplies the actual vendor directory and terminal output; +there are no hidden construction defaults or service lookups inside domain +services. The container is used only at this composition boundary. Project-scope validation, Git selection, PHP filtering and tool execution have separate responsibilities. The process transport receives its output sink; reusable services do not write to global standard-output handles. @@ -103,6 +109,31 @@ The initial preset overrides `method_argument_space.on_multiline` to `ignore`: the upstream `ensure_fully_multiline` mode collapses these deliberate blank lines. Standard comma spacing still applies; no custom fixer is introduced. +## Published framework composition + +This increment consumes `fast-forward/container` 1.10.1 (release commit +`a75dda2c8a9bd9cb2ef5bb25abc1a6b09de8a298`), `fast-forward/config` 1.7.3 and +`container-interop/service-provider` 0.4.1 through the actual Composer graph. +These are runtime library dependencies of the tool. Their development tools +are not transitively installed. No unpublished branch/PR or future Container +PR #49 behavior is required. The provider API is experimental (0.4) and distinct +from PSR-11's lookup contract; inspect compatibility when updating dependencies. + +Use the published `ContainerFactory::create()` for composition. Consumers needing +explicit bindings may pass one native `AggregateServiceProvider` containing the +module provider first and their `ArrayServiceProvider` last. Later factories +replace earlier factories before services are resolved. Keep replacements typed +to the declared interface and apply them before first lookup. Tests exercise +lazy construction, shared application identity, nested transport/output +overrides, native command binding and exact failure status with the published +adapter. No module-specific container, provider registry or override DSL exists. + +The default provider defines no extensions. This tool does not claim additional +cross-provider decoration or alias guarantees beyond the published adapter. +CPX installation isolation does not alter these in-process runtime/autoload +boundaries. Command configuration does not double as dependency injection: +check/fix are separate invocable entrypoints and transport authority is explicit. + ## Consequences and next gates This small slice validates syntax and PSR-12 style. Type analysis and semantic diff --git a/src/DependencyInjection/StandardsServiceProvider.php b/src/DependencyInjection/StandardsServiceProvider.php new file mode 100644 index 0000000..4496e98 --- /dev/null +++ b/src/DependencyInjection/StandardsServiceProvider.php @@ -0,0 +1,64 @@ + Lazy bindings; later aggregate-provider factories may replace them. */ + public function getFactories(): array + { + return [ + OutputInterface::class => fn (): OutputInterface => $this->output, + ProcessRunner::class => fn (ContainerInterface $container): ProcessRunner => $this->process($container->get(OutputInterface::class)), + ProjectScopeResolver::class => static fn (): ProjectScopeResolver => new ProjectScopeResolver(dirname(__DIR__, 2) . '/config/ecs.php'), + StandardsRunner::class => fn (ContainerInterface $container): StandardsRunner => new StandardsRunner( + $container->get(ProcessRunner::class), + $this->vendorDirectory . '/symplify/easy-coding-standard/bin/ecs', + ), + ]; + } + + /** @return array No default decorators; consumer extensions use the published provider contract. */ + public function getExtensions(): array + { + return []; + } + + /** Binds raw subprocess bytes to the supplied output while retaining stderr when supported. */ + private function process(OutputInterface $output): ProcessRunner + { + return new NativeProcessRunner(static function (string $type, string $message) use ($output): void { + $stream = Process::ERR === $type && $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + $stream->write($message, options: OutputInterface::OUTPUT_RAW); + }); + } +} diff --git a/tests/DependencyInjection/CompositionTest.php b/tests/DependencyInjection/CompositionTest.php new file mode 100644 index 0000000..d46a2c3 --- /dev/null +++ b/tests/DependencyInjection/CompositionTest.php @@ -0,0 +1,125 @@ + false, 'fix' => true] as $name => $write) { + $process = new RecordingTransport(); + $constructions = 0; + $override = new ArrayServiceProvider([ + ProcessRunner::class => static function () use ($process, &$constructions): ProcessRunner { + ++$constructions; + + return $process; + }, + ]); + $container = ContainerFactory::create(new AggregateServiceProvider( + new StandardsServiceProvider(dirname(__DIR__, 2) . '/vendor', new BufferedOutput()), + $override, + )); + self::assertTrue($container->has(Application::class)); + self::assertSame(0, $constructions); + + $application = $container->get(Application::class); + self::assertSame($application, $container->get(Application::class)); + self::assertSame(1, $constructions); + self::assertInstanceOf(CheckCommand::class, $container->get(CheckCommand::class)); + self::assertInstanceOf(FixCommand::class, $container->get(FixCommand::class)); + self::assertFalse(is_subclass_of(CheckCommand::class, Command::class)); + self::assertFalse(is_subclass_of(FixCommand::class, Command::class)); + + $tester = new ApplicationTester($application); + self::assertSame(23, $tester->run(['command' => $name, '--all' => true, '--directory' => dirname(__DIR__, 2)])); + self::assertSame($write, in_array('--fix', $process->commands[array_key_last($process->commands)], true)); + self::assertStringContainsString('Selected ', $tester->getDisplay()); + } + } + + /** A later output binding is used by the real native transport without interpreting subprocess markup. */ + public function testOutputOverridePreservesRawProcessBytes(): void + { + $default = new BufferedOutput(); + $replacement = new BufferedOutput(); + $container = ContainerFactory::create(new AggregateServiceProvider( + new StandardsServiceProvider(dirname(__DIR__, 2) . '/vendor', $default), + new ArrayServiceProvider([OutputInterface::class => static fn (): OutputInterface => $replacement]), + )); + + $status = $container->get(ProcessRunner::class)->run([PHP_BINARY, '-r', 'echo "literal";'], sys_get_temp_dir()); + self::assertSame(0, $status); + self::assertSame('', $default->fetch()); + self::assertSame('literal', $replacement->fetch()); + } + + /** A failed explicit dependency remains a construction error with its original cause. */ + public function testFailedBindingPreservesItsCause(): void + { + $cause = new \RuntimeException('Synthetic transport construction failure.'); + $container = ContainerFactory::create(new AggregateServiceProvider( + new StandardsServiceProvider(dirname(__DIR__, 2) . '/vendor', new BufferedOutput()), + new ArrayServiceProvider([ProcessRunner::class => static fn (): ProcessRunner => throw $cause]), + )); + + try { + $container->get(Application::class); + self::fail('Failed binding must not be replaced by an implicit default.'); + } catch (\Psr\Container\ContainerExceptionInterface $failure) { + while (null !== $failure->getPrevious()) { + $failure = $failure->getPrevious(); + } + + self::assertSame($cause, $failure); + } + } + + /** Invalid installation paths fail before building any service graph. */ + public function testInvalidVendorDirectoryHasAnObjectiveError(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('readable installed ECS entrypoint'); + new StandardsServiceProvider('', new BufferedOutput()); + } +} + +/** Captures tool authority/status while using real read-only Git selection in this checkout. */ +final class RecordingTransport implements ProcessRunner +{ + /** @var list> Tool arguments observed without executing syntax/style processes. */ + public array $commands = []; + + /** Reads only Git metadata through the production shell-free transport. */ + public function capture(array $arguments, string $directory): string + { + return (new NativeProcessRunner())->capture($arguments, $directory); + } + + /** Returns a synthetic style failure to verify end-to-end exit-code propagation. */ + public function run(array $arguments, string $directory): int + { + $this->commands[] = $arguments; + + return in_array('-l', $arguments, true) ? 0 : 23; + } +}