diff --git a/.agents/skills/phpunit-tests/SKILL.md b/.agents/skills/phpunit-tests/SKILL.md index f568918b0..57218971f 100644 --- a/.agents/skills/phpunit-tests/SKILL.md +++ b/.agents/skills/phpunit-tests/SKILL.md @@ -83,4 +83,57 @@ Choose the smallest command that proves the change. - Re-run after fixes until the targeted tests pass. - If failures come from unrelated pre-existing breakage, call that out separately and do not silently claim success. +### Composer plugin validation flow + +Some repository test scenarios are only valid when DevTools is executed as a Composer plugin. +Create a temporary fixture under `backup/` before running plugin-based verification. + +```bash +PROJECT_ROOT="$(pwd)" +PLUGIN_FIXTURE="$PROJECT_ROOT/backup/composer-plugin-consumer" +mkdir -p "$PROJECT_ROOT/backup" +rm -rf "$PLUGIN_FIXTURE" +mkdir -p "$PLUGIN_FIXTURE" + +cat > "$PLUGIN_FIXTURE/composer.json" <<'JSON' +{ + "name": "fast-forward/dev-tools-composer-plugin-consumer-fixture", + "description": "Fixture project used to validate DevTools plugin behavior.", + "license": "MIT", + "type": "project", + "require": { + "fast-forward/dev-tools": "*@dev" + }, + "repositories": [ + { + "type": "path", + "url": "../..", + "options": { + "symlink": true + } + } + ], + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "fast-forward/dev-tools": true, + "phpdocumentor/shim": true, + "phpro/grumphp-shim": true, + "pyrech/composer-changelogs": true + } + }, + "scripts": { + "dev-tools": "dev-tools", + "dev-tools:fix": "@dev-tools --fix" + } +} +JSON + +cd "$PLUGIN_FIXTURE" +composer install +composer tests +``` + Read [references/generation-checklist.md](references/generation-checklist.md) before writing or repairing tests in an unfamiliar repository. diff --git a/.github/wiki b/.github/wiki index 3dea0d478..c99c7cd82 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 3dea0d4783241698c7c8b275906f12f6e22d1212 +Subproject commit c99c7cd826a90dc2aa8c85cfe7dd05b4186904c4 diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index af8cc07d7..01a76f7b7 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,6 +2,10 @@ declare(strict_types=1); +require __DIR__ . '/vendor/autoload.php'; + +use FastForward\DevTools\Path\WorkingProjectPathResolver; + $rules = [ 'phpdoc_indent' => true, 'phpdoc_order' => [ @@ -39,11 +43,7 @@ $finder = PhpCsFixer\Finder::create() ->in([getcwd()]) - ->exclude('public') - ->exclude('resources') - ->exclude('vendor') - ->exclude('tmp') -; + ->exclude(WorkingProjectPathResolver::TOOLING_EXCLUDED_DIRECTORIES); return (new PhpCsFixer\Config()) ->setRiskyAllowed(false) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd08bcdd..ca254b52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add a hybrid command runtime bootstrap and capability bridge that keeps command discovery split between migrated Symfony commands (`DevTools`) and legacy Composer `BaseCommand` commands (`DevToolsComposer`) while exposing proxy commands during Composer execution for the first migration step (#199) + +### Changed + +- Restore `standards:phpdoc` as the command entry point for PHPDoc checks while keeping + the legacy aliases `docheader` and `php-cs-fixer`. +- Remove the `phpdoc` alias from `reports:docs` to avoid command-name collisions with + the standards PHPDoc check command. + ## [1.22.3] - 2026-04-25 ### Fixed diff --git a/bin/dev-tools.php b/bin/dev-tools.php index c3fc7f46f..d3eba1dae 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -20,11 +20,10 @@ namespace FastForward\DevTools; use FastForward\DevTools\Console\DevTools; -use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; $pluginVendorAutoload = \dirname(__DIR__) . '/vendor/autoload.php'; require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -DevTools::create()->run(new ArgvInput([...$argv, '--no-plugins'])); +DevTools::create()->run(); diff --git a/composer.json b/composer.json index 0362b1ec7..942bd8aa7 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,6 @@ "require": { "php": "^8.3", "composer-plugin-api": "^2.0", - "composer/composer": "^2.9", "container-interop/service-provider": "^0.4.1", "dg/bypass-finals": "^1.9", "ergebnis/agent-detector": "^1.1", @@ -82,6 +81,9 @@ "thecodingmachine/safe": "^3.4", "twig/twig": "^3.24" }, + "require-dev": { + "composer/composer": "^2.9" + }, "minimum-stability": "stable", "autoload": { "psr-4": { diff --git a/docs/api/commands.rst b/docs/api/commands.rst index e31e851bc..6fa37a06b 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -37,8 +37,9 @@ subprocess execution is needed. - ``refactor`` - Runs Rector with local or packaged configuration. * - ``FastForward\DevTools\Console\Command\PhpDocCommand`` - - ``phpdoc`` - - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. + - ``standards:phpdoc`` + - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. Supported aliases: + ``phpdoc``, ``docheader`` and ``php-cs-fixer``. * - ``FastForward\DevTools\Console\Command\CodeStyleCommand`` - ``code-style`` - Runs Composer Normalize and ECS. diff --git a/docs/commands/funding.rst b/docs/commands/funding.rst index ad95c8739..3910d6e65 100644 --- a/docs/commands/funding.rst +++ b/docs/commands/funding.rst @@ -33,7 +33,7 @@ Options ``--composer-file`` (optional) Path to the Composer manifest to synchronize. Default: - ``Factory::getComposerFile()``. + ``composer.json``. ``--funding-file`` (optional) Path to the GitHub funding file to synchronize. Default: diff --git a/docs/commands/metrics.rst b/docs/commands/metrics.rst index 0005663d2..c7ca5794b 100644 --- a/docs/commands/metrics.rst +++ b/docs/commands/metrics.rst @@ -33,7 +33,7 @@ Options Comma-separated directories that should be excluded from analysis. Default: - ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures``. + ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources``. ``--target=`` Output directory for the generated metrics reports. diff --git a/docs/commands/phpdoc.rst b/docs/commands/phpdoc.rst index 58e5d95ef..47f1c28c2 100644 --- a/docs/commands/phpdoc.rst +++ b/docs/commands/phpdoc.rst @@ -7,6 +7,7 @@ Description ----------- The ``phpdoc`` command coordinates PHPDoc checking and fixing using: +(alias ``docheader`` and ``php-cs-fixer``) - PHP-CS-Fixer - fixes PHPDoc formatting - Rector with ``AddMissingMethodPhpDocRector`` - adds missing method PHPDoc diff --git a/src/Changelog/Checker/UnreleasedEntryChecker.php b/src/Changelog/Checker/UnreleasedEntryChecker.php index b9fb9b289..2aaafdc26 100644 --- a/src/Changelog/Checker/UnreleasedEntryChecker.php +++ b/src/Changelog/Checker/UnreleasedEntryChecker.php @@ -74,7 +74,7 @@ public function hasPendingChanges(string $file, ?string $againstReference = null } try { - $baseline = $this->gitClient->show($againstReference, $file, $this->filesystem->dirname($file)); + $baseline = $this->gitClient->show($againstReference, $file, $this->filesystem->getDirectory($file)); } catch (Throwable) { return true; } diff --git a/src/Changelog/Manager/ChangelogManager.php b/src/Changelog/Manager/ChangelogManager.php index 8372f0518..72b3e0135 100644 --- a/src/Changelog/Manager/ChangelogManager.php +++ b/src/Changelog/Manager/ChangelogManager.php @@ -189,7 +189,7 @@ private function persist(string $file, ChangelogDocument $document): void { $this->filesystem->dumpFile( $file, - $this->renderer->render($document, $this->resolveRepositoryUrl($this->filesystem->dirname($file))), + $this->renderer->render($document, $this->resolveRepositoryUrl($this->filesystem->getDirectory($file))), ); } } diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 3322a87e8..76501fdfb 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -19,8 +19,8 @@ namespace FastForward\DevTools\Composer\Capability; -use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; +use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Console\DevTools; /** @@ -29,14 +29,35 @@ */ final class DevToolsCommandProvider implements CommandProvider { + /** + * @var string the namespace prefix for dev-tools console commands to be registered as Composer commands + */ + private const string COMMAND_NAMESPACE = 'FastForward\DevTools\Console\Command'; + /** * {@inheritDoc} */ public function getCommands() { - return array_values(array_filter( - DevTools::create()->all(), - static fn(object $command): bool => $command instanceof BaseCommand, - )); + $commands = []; + + foreach (DevTools::create()->all() as $registeredName => $command) { + /** + * Composer plugin registrations must be canonicalized to one command per Symfony command. + * The application exposes alias keys in `all()`, but Composer interprets each entry as + * an independent command and emits override warnings. + */ + if ($registeredName !== $command->getName()) { + continue; + } + + if (! str_starts_with($command::class, self::COMMAND_NAMESPACE)) { + continue; + } + + $commands[] = new ProxyCommand($command); + } + + return $commands; } } diff --git a/src/Composer/Command/ProxyCommand.php b/src/Composer/Command/ProxyCommand.php new file mode 100644 index 000000000..e9ab2c4df --- /dev/null +++ b/src/Composer/Command/ProxyCommand.php @@ -0,0 +1,58 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Composer\Command; + +use Composer\Command\BaseCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Adapts migrated Symfony commands to Composer's BaseCommand contract. + */ +final class ProxyCommand extends BaseCommand +{ + /** + * @param Command $command the Symfony command adapted for Composer plugin execution + */ + public function __construct( + private readonly Command $command, + ) { + parent::__construct($this->command->getName()); + + $this + ->setAliases($this->command->getAliases()) + ->setDescription($this->command->getDescription()) + ->setHelp($this->command->getHelp()) + ->setDefinition(clone $this->command->getDefinition()) + ->setHidden($this->command->isHidden()); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + return $this->command->run($input, $output); + } +} diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index bdb6a5fd5..8c3acb5a9 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -20,19 +20,18 @@ namespace FastForward\DevTools\Composer\Json; use RuntimeException; -use UnexpectedValueException; -use Composer\Factory; use Composer\InstalledVersions; -use Composer\Json\JsonFile; use DateTimeImmutable; use FastForward\DevTools\Composer\Json\Schema\Author; use FastForward\DevTools\Composer\Json\Schema\AuthorInterface; use FastForward\DevTools\Composer\Json\Schema\Funding; use FastForward\DevTools\Composer\Json\Schema\Support; use FastForward\DevTools\Composer\Json\Schema\SupportInterface; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use UnderflowException; -use function Safe\realpath; +use function Safe\file_get_contents; +use function Safe\json_decode; /** * Represents a specialized reader for a Composer JSON file. @@ -83,17 +82,17 @@ final class ComposerJson implements ComposerJsonInterface * default Composer file path SHALL be used. * * @throws RuntimeException when $path is'nt provided and COMPOSER environment variable is set to a directory - * @throws UnexpectedValueException when composer.json can't be parsed + * @throws RuntimeException when composer manifest files cannot be read or parsed */ public function __construct(?string $path = null) { - $pathLocal = realpath(Factory::getComposerFile()); + $pathLocal = WorkingProjectPathResolver::getProjectPath('composer.json'); $path ??= $pathLocal; $installedJsonPath = \dirname($pathLocal) . '/vendor/composer/installed.json'; - $this->data = (new JsonFile($path))->read(); - $this->installed = (new JsonFile($installedJsonPath))->read(); + $this->data = $this->readComposerJsonFile($path); + $this->installed = $this->readComposerInstalledManifest($installedJsonPath); } /** @@ -206,7 +205,7 @@ public function getReadme(): string */ public function getTime(): ?DateTimeImmutable { - $packages = $this->installed['packages']; + $packages = $this->installed['packages'] ?? []; if (isset($packages[$this->getName()])) { return new DateTimeImmutable($packages[$this->getName()]['time']); @@ -584,4 +583,56 @@ public function getComments(): array return \is_array($comments) ? $comments : []; } + + /** + * Reads and decodes a composer manifest file. + * + * @param string $path the manifest path + * + * @return array the parsed payload + */ + private function readComposerJsonFile(string $path): array + { + if (! file_exists($path)) { + throw new RuntimeException(\sprintf('Unable to read composer manifest file at path: %s', $path)); + } + + return $this->decodeJson($path); + } + + /** + * Reads and decodes the composer installed manifest. + * + * @param string $path installed manifest path + * + * @return array the parsed payload + */ + private function readComposerInstalledManifest(string $path): array + { + if (! file_exists($path)) { + return []; + } + + return $this->decodeJson($path); + } + + /** + * Decodes a JSON file. + * + * @param string $path the file path + * + * @return array the decoded payload + */ + private function decodeJson(string $path): array + { + $contents = file_get_contents($path); + + if (false === $contents) { + throw new RuntimeException(\sprintf('Unable to read composer manifest file at path: %s', $path)); + } + + $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); + + return \is_array($data) ? $data : []; + } } diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index a0f88b476..f62f95f4b 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -152,6 +152,7 @@ public static function applyPackagedRepositoryIgnores(Configuration $configurati self::DEFAULT_PACKAGED_PROD_ONLY_IN_DEV_DEPENDENCIES, [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV], ); + $configuration->ignoreErrorsOnPackage('composer/composer', [ErrorType::DEV_DEPENDENCY_IN_PROD]); return $configuration; } diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index dc0cc2028..d26595ac3 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -20,21 +20,25 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Synchronizes packaged Fast Forward project agents into the consumer repository. */ -#[AsCommand(name: 'agents', description: 'Synchronizes Fast Forward project agents into .agents/agents directory.')] -final class AgentsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'agents:agents', + description: 'Synchronizes Fast Forward project agents into .agents/agents directory.', + aliases: ['agents'], +)] +final class AgentsCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -98,8 +102,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->info('Created .agents/agents directory.'); } - $this->synchronizer->setLogger($this->getIO()); - $result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY); if ($result->failed()) { diff --git a/src/Console/Command/ChangelogCheckCommand.php b/src/Console/Command/ChangelogCheckCommand.php index 4b9aef41a..eaebc859b 100644 --- a/src/Console/Command/ChangelogCheckCommand.php +++ b/src/Console/Command/ChangelogCheckCommand.php @@ -20,12 +20,12 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -37,7 +37,7 @@ name: 'changelog:check', description: 'Checks whether a changelog file contains meaningful unreleased entries.' )] -final class ChangelogCheckCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogCheckCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogEntryCommand.php b/src/Console/Command/ChangelogEntryCommand.php index 827489b2c..7de3a385f 100644 --- a/src/Console/Command/ChangelogEntryCommand.php +++ b/src/Console/Command/ChangelogEntryCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Document\ChangelogDocument; use FastForward\DevTools\Changelog\Entry\ChangelogEntryType; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; @@ -28,6 +27,7 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,7 +40,7 @@ name: 'changelog:entry', description: 'Adds a changelog entry to Unreleased or a specific version section.' )] -final class ChangelogEntryCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogEntryCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogNextVersionCommand.php b/src/Console/Command/ChangelogNextVersionCommand.php index f3837a8be..18b940070 100644 --- a/src/Console/Command/ChangelogNextVersionCommand.php +++ b/src/Console/Command/ChangelogNextVersionCommand.php @@ -21,12 +21,12 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -38,7 +38,7 @@ name: 'changelog:next-version', description: 'Infers the next semantic version from the Unreleased changelog section.' )] -final class ChangelogNextVersionCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogNextVersionCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogPromoteCommand.php b/src/Console/Command/ChangelogPromoteCommand.php index 4f7266ec0..6425f0715 100644 --- a/src/Console/Command/ChangelogPromoteCommand.php +++ b/src/Console/Command/ChangelogPromoteCommand.php @@ -21,13 +21,13 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -40,7 +40,7 @@ name: 'changelog:promote', description: 'Promotes Unreleased entries into a published changelog version.' )] -final class ChangelogPromoteCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogPromoteCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ChangelogShowCommand.php b/src/Console/Command/ChangelogShowCommand.php index 9d04e9488..dbc54c59e 100644 --- a/src/Console/Command/ChangelogShowCommand.php +++ b/src/Console/Command/ChangelogShowCommand.php @@ -21,12 +21,12 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use Throwable; -use Composer\Command\BaseCommand; use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -36,7 +36,7 @@ * Prints the rendered notes body for a released changelog version. */ #[AsCommand(name: 'changelog:show', description: 'Prints the notes body for a released changelog version.')] -final class ChangelogShowCommand extends BaseCommand implements LoggerAwareCommandInterface +final class ChangelogShowCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index 5173ac63a..669109f13 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\CodeOwners\CodeOwnersGenerator; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -28,15 +27,22 @@ use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Generates and synchronizes CODEOWNERS files from local project metadata. */ -#[AsCommand(name: 'codeowners', description: 'Generates .github/CODEOWNERS from local project metadata.')] -final class CodeOwnersCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'github:codeowners', + description: 'Generates .github/CODEOWNERS from local project metadata.', + aliases: ['.github/CODEOWNERS', 'codeowners'], +)] +final class CodeOwnersCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -48,12 +54,14 @@ final class CodeOwnersCommand extends BaseCommand implements LoggerAwareCommandI * @param FilesystemInterface $filesystem the filesystem used to read and write the target file * @param FileDiffer $fileDiffer the differ used to report managed-file drift * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the SymfonyStyle instance for interactive prompts */ public function __construct( private readonly CodeOwnersGenerator $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -109,7 +117,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $targetPath = $this->filesystem->getAbsolutePath((string) $input->getOption('file')); - $targetDirectory = $this->filesystem->dirname($targetPath); + $targetDirectory = $this->filesystem->getDirectory($targetPath); $overwrite = (bool) $input->getOption('overwrite'); $dryRun = (bool) $input->getOption('dry-run'); $check = (bool) $input->getOption('check'); @@ -207,13 +215,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function promptForOwners(): array { - $answer = (string) $this->getIO() - ->ask( - 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', - '', - ); + $answer = (string) $this->io->ask( + 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', + ); - return $this->generator->normalizeOwners($answer); + return $this->generator->normalizeOwners($answer ?? ''); } /** @@ -225,7 +231,14 @@ private function promptForOwners(): array */ private function shouldWriteCodeOwners(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf( + 'The generated CODEOWNERS file differs from the existing file at %s. Overwrite? [y/N] ', + $targetPath + ), + false, + ); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 28114575e..d9b35f18b 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -37,10 +37,11 @@ * This class MUST NOT be overridden and SHALL rely on external tools like ECS and Composer Normalize. */ #[AsCommand( - name: 'code-style', - description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.' + name: 'standards:code-style', + description: 'Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.', + aliases: ['code-style'] )] -final class CodeStyleCommand extends BaseCommand implements LoggerAwareCommandInterface +final class CodeStyleCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 329f69e41..950a485df 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -29,16 +28,23 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Path; /** * Copies packaged or local resources into the consumer repository. */ -#[AsCommand(name: 'copy-resource', description: 'Copies a file or directory resource into the current project.')] -final class CopyResourceCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'dev-tools:sync:copy', + description: 'Copies a file or directory resource into the current project.', + aliases: ['copy-resource'] +)] +final class CopyResourceCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -51,6 +57,7 @@ final class CopyResourceCommand extends BaseCommand implements LoggerAwareComman * @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources * @param FileDiffer $fileDiffer the service used to summarize overwrite changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -58,6 +65,7 @@ public function __construct( private readonly FinderFactoryInterface $finderFactory, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -309,7 +317,11 @@ private function copyFile( */ private function shouldReplaceResource(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Replace drifted resource %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf('Replace drifted resource %s? [y/N] ', $targetPath), + false, + ); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index e80c4481b..0d2c5b117 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig; use FastForward\DevTools\Process\ProcessBuilderInterface; @@ -29,6 +28,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -43,11 +43,11 @@ * deterministic report that is friendly for local development and CI runs. */ #[AsCommand( - name: 'dependencies', + name: 'dev-tools:deps', description: 'Analyzes missing, unused, misplaced, and outdated Composer dependencies.', - aliases: ['deps'] + aliases: ['deps', 'dependencies'] )] -final class DependenciesCommand extends BaseCommand implements LoggerAwareCommandInterface +final class DependenciesCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index a5822dfe3..5cc9a460b 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -24,13 +24,13 @@ use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use Twig\Environment; -use Composer\Command\BaseCommand; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -46,8 +46,12 @@ * queue so logging and grouped output stay consistent with the rest of the * command surface. */ -#[AsCommand(name: 'docs', description: 'Generates API documentation.')] -final class DocsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'reports:docs', + description: 'Generates API documentation.', + aliases: ['reports:phpdoc', 'phpDocumentor', 'docs'], +)] +final class DocsCommand extends Command { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index 58d1f528e..966e487f8 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; -use Composer\Factory; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -32,18 +30,22 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Synchronizes funding metadata between composer.json and .github/FUNDING.yml. */ #[AsCommand( - name: 'funding', - description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.' + name: 'github:funding', + description: 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.', + aliases: ['.github/FUNDING.yml', 'composer:funding', 'funding'], )] -final class FundingCommand extends BaseCommand implements LoggerAwareCommandInterface +final class FundingCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -59,6 +61,7 @@ final class FundingCommand extends BaseCommand implements LoggerAwareCommandInte * @param ProcessBuilderInterface $processBuilder the process builder used to normalize composer.json after updates * @param ProcessQueueInterface $processQueue the process queue used to execute composer normalize * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -69,6 +72,7 @@ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -88,7 +92,7 @@ protected function configure(): void name: 'composer-file', mode: InputOption::VALUE_OPTIONAL, description: 'Path to the composer.json file to synchronize.', - default: Factory::getComposerFile(), + default: 'composer.json', ) ->addOption( name: 'funding-file', @@ -449,7 +453,7 @@ private function handleFundingFile( ); } - $this->filesystem->mkdir($this->filesystem->dirname($fundingFile)); + $this->filesystem->mkdir($this->filesystem->getDirectory($fundingFile)); $this->filesystem->dumpFile($fundingFile, $updatedFundingContents); return $this->success( @@ -470,8 +474,9 @@ private function handleFundingFile( */ private function shouldWriteManagedFile(string $targetFile): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetFile), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetFile), false); + + return $this->io->askQuestion($confirmation); } /** @@ -488,13 +493,13 @@ private function normalizeComposerFile(string $composerFile, OutputInterface $ou ->withArgument('--ansi') ->withArgument('--no-update-lock'); - $workingDirectory = $this->filesystem->dirname($composerFile); + $workingDirectory = $this->filesystem->getDirectory($composerFile); if ('.' !== $workingDirectory) { $processBuilder = $processBuilder->withArgument('--working-dir', $workingDirectory); } - $composerBasename = $this->filesystem->basename($composerFile); + $composerBasename = $this->filesystem->getBasename($composerFile); if ('composer.json' !== $composerBasename) { $processBuilder = $processBuilder->withArgument('--file', $composerBasename); diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index 37f9677f6..fe4fec7f9 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; @@ -33,9 +32,12 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use function Safe\getcwd; @@ -46,10 +48,11 @@ * to keep them out of Composer package archives. */ #[AsCommand( - name: 'gitattributes', - description: 'Manages .gitattributes export-ignore rules for leaner package archives.' + name: 'git:attributes', + description: 'Manages .gitattributes export-ignore rules for leaner package archives.', + aliases: ['.gitattributes', 'gitattributes'], )] -final class GitAttributesCommand extends BaseCommand implements LoggerAwareCommandInterface +final class GitAttributesCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -73,8 +76,9 @@ final class GitAttributesCommand extends BaseCommand implements LoggerAwareComma * @param WriterInterface $writer the writer component * @param FilesystemInterface $filesystem the filesystem component * @param ComposerJsonInterface $composer the composer.json accessor - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly CandidateProviderInterface $candidateProvider, @@ -87,6 +91,7 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -259,8 +264,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteGitAttributes(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } /** diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index 82c4b8d1f..be98f4912 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FinderFactoryInterface; @@ -28,17 +27,24 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Exception\IOExceptionInterface; use Symfony\Component\Filesystem\Path; /** * Installs packaged Git hooks for the consumer repository. */ -#[AsCommand(name: 'git-hooks', description: 'Installs Fast Forward Git hooks.')] -final class GitHooksCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'git:hooks', + description: 'Installs Fast Forward Git hooks.', + aliases: ['.git/hooks', 'git-hooks'], +)] +final class GitHooksCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -49,8 +55,9 @@ final class GitHooksCommand extends BaseCommand implements LoggerAwareCommandInt * @param FilesystemInterface $filesystem the filesystem used to copy hooks * @param FileLocatorInterface $fileLocator the locator used to find packaged hooks * @param FinderFactoryInterface $finderFactory the factory used to create finders for hook files - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly FilesystemInterface $filesystem, @@ -58,6 +65,7 @@ public function __construct( private readonly FinderFactoryInterface $finderFactory, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -259,8 +267,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldReplaceHook(string $hookPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Replace drifted Git hook %s? [y/N] ', $hookPath), false); + $confirmation = new ConfirmationQuestion( + \sprintf('Replace drifted Git hook %s? [y/N] ', $hookPath), + false, + ); + + return $this->io->askQuestion($confirmation); } /** @@ -293,7 +305,7 @@ private function installHook( 'Failed to install {hook_name} hook automatically. Remove or unlock {hook_path} and rerun git-hooks.', [ 'input' => $input, - 'hook_name' => $this->filesystem->basename($hookPath), + 'hook_name' => $this->filesystem->getBasename($hookPath), 'hook_path' => $hookPath, 'error' => $ioException->getMessage(), 'file' => $ioException->getPath() ?? $hookPath, diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index 92be06ebb..57bc7072e 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\GitIgnore\MergerInterface; use FastForward\DevTools\GitIgnore\ReaderInterface; @@ -31,9 +30,12 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Provides functionality to merge and synchronize .gitignore files. @@ -44,8 +46,12 @@ * The command accepts two options: --source and --target to specify the paths * to the canonical and project .gitignore files respectively. */ -#[AsCommand(name: 'gitignore', description: 'Merges and synchronizes .gitignore files.')] -final class GitIgnoreCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'git:ignore', + description: 'Merges and synchronizes .gitignore files.', + aliases: ['.gitignore', 'gitignore'], +)] +final class GitIgnoreCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -62,8 +68,9 @@ final class GitIgnoreCommand extends BaseCommand implements LoggerAwareCommandIn * @param ReaderInterface $reader the reader component * @param WriterInterface|null $writer the writer component * @param FileLocatorInterface $fileLocator the file locator - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly MergerInterface $merger, @@ -72,6 +79,7 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -214,7 +222,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteGitIgnore(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Update managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index 79aa829a0..94314345b 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -27,9 +26,12 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; /** * Generates and copies LICENSE files to projects. @@ -37,8 +39,12 @@ * This command generates a LICENSE file if one does not exist and a supported * license is declared in composer.json. */ -#[AsCommand(name: 'license', description: 'Generates a LICENSE file from composer.json license information.')] -final class LicenseCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'license:generate', + description: 'Generates a LICENSE file from composer.json license information.', + aliases: ['LICENSE.md', 'license'], +)] +final class LicenseCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -48,14 +54,16 @@ final class LicenseCommand extends BaseCommand implements LoggerAwareCommandInte * * @param GeneratorInterface $generator the generator component * @param FilesystemInterface $filesystem the filesystem component - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly GeneratorInterface $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -230,7 +238,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldWriteLicense(string $targetPath): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + $confirmation = new ConfirmationQuestion(\sprintf('Write managed file %s? [y/N] ', $targetPath), false); + + return $this->io->askQuestion($confirmation); } } diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 59471a96b..434dbf620 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -34,8 +34,12 @@ use function rtrim; -#[AsCommand(name: 'metrics', description: 'Analyzes code metrics with PhpMetrics.')] -final class MetricsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'reports:metrics', + description: 'Analyzes code metrics with PhpMetrics.', + aliases: ['reports:phpmetrics', 'phpmetrics', 'metrics'], +)] +final class MetricsCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -85,7 +89,7 @@ protected function configure(): void name: 'exclude', mode: InputOption::VALUE_OPTIONAL, description: 'Comma-separated directories that SHOULD be excluded from analysis.', - default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,tests/Fixtures', + default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources', ) ->addOption( name: 'target', diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 75cf08129..579defb9f 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -30,11 +30,11 @@ use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Twig\Environment; -use Composer\Command\BaseCommand; use Throwable; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -44,8 +44,12 @@ * Provides operations to inspect, lint, and repair PHPDoc comments across the project. * The class MUST NOT be extended and SHALL coordinate tools like PHP-CS-Fixer and Rector. */ -#[AsCommand(name: 'phpdoc', description: 'Checks and fixes PHPDocs.')] -final class PhpDocCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand(name: 'standards:phpdoc', description: 'Checks and fixes PHPDocs.', aliases: [ + 'phpdoc', + 'docheader', + 'php-cs-fixer', +])] +final class PhpDocCommand extends Command { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index 8a7fb769d..6f2835104 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -36,8 +36,12 @@ * Provides functionality to execute automated code refactoring using Rector. * This class MUST NOT be extended and SHALL encapsulate the logic for Rector invocation. */ -#[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: ['rector'])] -final class RefactorCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'standards:rector', + description: 'Runs Rector for code refactoring.', + aliases: ['refactor', 'rector'] +)] +final class RefactorCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index d896c6afa..b11e45c3a 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -20,16 +20,17 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; -use Composer\Console\Input\InputOption; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -37,8 +38,12 @@ * Coordinates the generation of Fast Forward documentation frontpage and related reports. * This class MUST NOT be overridden and SHALL securely combine docs and testing commands. */ -#[AsCommand(name: 'reports', description: 'Generates the frontpage for Fast Forward documentation.')] -final class ReportsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'standards:reports', + description: 'Generates the frontpage for Fast Forward documentation.', + aliases: ['reports'], +)] +final class ReportsCommand extends Command { use HasCacheOption; use HasJsonOption; @@ -165,7 +170,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $docsBuilder = $docsBuilder->withArgument('--pretty-json'); } - $docs = $docsBuilder->build('composer dev-tools docs --'); + $docs = $docsBuilder->build(DevToolsPathResolver::getBinaryCommand('docs')); if (null !== $cacheArgument) { $coverageBuilder = $coverageBuilder->withArgument($cacheArgument); @@ -187,7 +192,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $coverageBuilder = $coverageBuilder->withArgument('--pretty-json'); } - $coverage = $coverageBuilder->build('composer dev-tools tests --'); + $coverage = $coverageBuilder->build(DevToolsPathResolver::getBinaryCommand('tests')); if ($progress) { $metricsBuilder = $metricsBuilder->withArgument('--progress'); @@ -201,7 +206,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $metricsBuilder = $metricsBuilder->withArgument('--pretty-json'); } - $metrics = $metricsBuilder->build('composer dev-tools metrics --'); + $metrics = $metricsBuilder->build(DevToolsPathResolver::getBinaryCommand('metrics')); $this->processQueue->add(process: $docs, detached: true, label: 'Generating API Docs Report'); $this->processQueue->add(process: $coverage, label: 'Generating Coverage Report'); diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index 5a0cf0be2..726bec3e8 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -44,8 +44,12 @@ * target paths, triggers synchronization, and translates the resulting status * into Symfony Console output and process exit codes. */ -#[AsCommand(name: 'skills', description: 'Synchronizes Fast Forward skills into .agents/skills directory.')] -final class SkillsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'agents:skills', + description: 'Synchronizes Fast Forward skills into .agents/skills directory.', + aliases: ['skills'] +)] +final class SkillsCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -130,8 +134,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->info('Created .agents/skills directory.'); } - $this->synchronizer->setLogger($this->getIO()); - $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY); if ($result->failed()) { diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index e176644fb..c2dd4c221 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -20,14 +20,15 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -37,8 +38,12 @@ * Executes the full suite of Fast Forward code standard checks. * This class MUST NOT be modified through inheritance and SHALL streamline code validation workflows. */ -#[AsCommand(name: 'standards', description: 'Runs Fast Forward code standards checks.')] -final class StandardsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'dev-tools:standards', + description: 'Runs Fast Forward code standards checks.', + aliases: ['standards'], +)] +final class StandardsCommand extends Command { use HasCacheOption; use HasJsonOption; @@ -145,7 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->processQueue->add( - process: $processBuilder->build('composer dev-tools ' . $command . ' --'), + process: $processBuilder->build(DevToolsPathResolver::getBinaryCommand($command)), label: $this->getProcessLabel($command), ); } diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index c08fd2414..82094c42f 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -20,13 +20,13 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -37,9 +37,10 @@ */ #[AsCommand( name: 'dev-tools:sync', - description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.' + description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, CODEOWNERS, .editorconfig, and .gitattributes in the root project.', + aliases: ['sync'], )] -final class SyncCommand extends BaseCommand implements LoggerAwareCommandInterface +final class SyncCommand extends Command { use HasJsonOption; use LogsCommandResults; diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index d62ba27b7..9e050ee9d 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; @@ -34,6 +33,7 @@ use RuntimeException; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -46,8 +46,8 @@ * Facilitates the execution of the PHPUnit testing framework. * This class MUST NOT be overridden and SHALL configure testing parameters dynamically. */ -#[AsCommand(name: 'tests', description: 'Runs PHPUnit tests.')] -final class TestsCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand(name: 'reports:tests', description: 'Runs PHPUnit tests.', aliases: ['phpunit', 'tests'])] +final class TestsCommand extends Command { use HasCacheOption; use HasJsonOption; diff --git a/src/Console/Command/Traits/HasCommandLogger.php b/src/Console/Command/Traits/HasCommandLogger.php index 5bbcd0c2f..e4e21c8bb 100644 --- a/src/Console/Command/Traits/HasCommandLogger.php +++ b/src/Console/Command/Traits/HasCommandLogger.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command\Traits; -use FastForward\DevTools\Console\Command\LoggerAwareCommandInterface; use LogicException; use Psr\Log\LoggerInterface; @@ -39,15 +38,18 @@ trait HasCommandLogger */ public function getLogger(): LoggerInterface { - if ( - ! $this instanceof LoggerAwareCommandInterface - || (! property_exists($this, 'logger') || null === $this->logger) - || ! $this->logger instanceof LoggerInterface - ) { + if (! property_exists($this, 'logger') || null === $this->logger) { throw new LogicException(\sprintf( - 'Commands using %s MUST implement %s and expose an initialized $logger property with an instance of %s.', + 'Commands using %s MUST expose an initialized $logger property with an instance of %s.', + LogsCommandResults::class, + LoggerInterface::class, + )); + } + + if (! $this->logger instanceof LoggerInterface) { + throw new LogicException(\sprintf( + 'Commands using %s MUST expose a %s instance on the $logger property.', LogsCommandResults::class, - LoggerAwareCommandInterface::class, LoggerInterface::class, )); } diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index e4b4aa8fd..02aff4cf5 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -20,9 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; -use Composer\Factory; -use Composer\Json\JsonManipulator; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -32,21 +29,27 @@ use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Path; +use function Safe\json_decode; +use function Safe\json_encode; use function Safe\getcwd; /** * Updates composer.json with the Fast Forward dev-tools integration metadata. */ #[AsCommand( - name: 'update-composer-json', - description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.' + name: 'dev-tools:sync:composer', + description: 'Updates composer.json with Fast Forward dev-tools scripts and metadata.', + aliases: ['composer.json', 'update-composer-json'], )] -final class UpdateComposerJsonCommand extends BaseCommand implements LoggerAwareCommandInterface +final class UpdateComposerJsonCommand extends Command { use HasJsonOption; use LogsCommandResults; @@ -57,8 +60,9 @@ final class UpdateComposerJsonCommand extends BaseCommand implements LoggerAware * @param ComposerJsonInterface $composer the composer.json metadata accessor * @param FilesystemInterface $filesystem the filesystem used to read and write composer.json * @param FileLocatorInterface $fileLocator the locator used to resolve packaged configuration files - * @param FileDiffer $fileDiffer + * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes * @param LoggerInterface $logger the output-aware logger + * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly ComposerJsonInterface $composer, @@ -66,6 +70,7 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly FileDiffer $fileDiffer, private readonly LoggerInterface $logger, + private readonly SymfonyStyle $io, ) { parent::__construct(); } @@ -86,7 +91,7 @@ protected function configure(): void shortcut: 'f', mode: InputOption::VALUE_OPTIONAL, description: 'Path to the composer.json file to update.', - default: Factory::getComposerFile(), + default: 'composer.json', ) ->addOption( name: 'dry-run', @@ -132,22 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $currentContents = $this->filesystem->readFile($file); - $manipulator = new JsonManipulator($currentContents); - $grumphpConfig = DevToolsPathResolver::getPackagePath('grumphp.yml'); - - foreach ($this->scripts() as $name => $command) { - $manipulator->addSubNode('scripts', $name, $command); - } - - if ('' === $this->composer->getReadme() && $this->filesystem->exists('README.md', \dirname($file))) { - $manipulator->addProperty('readme', 'README.md'); - } - - $manipulator->addSubNode('extra', 'grumphp', [ - 'config-default-path' => Path::makeRelative($grumphpConfig, getcwd()), - ], true); - - $updatedContents = $manipulator->getContents(); + $updatedContents = $this->updatedComposerJsonContents($currentContents, $file); $comparison = $this->fileDiffer->diffContents( 'generated dev-tools composer.json configuration', $file, @@ -205,8 +195,69 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function shouldUpdateComposerJson(string $file): bool { - return $this->getIO() - ->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $file), false); + $confirmationMessage = \sprintf( + 'composer.json file %s has changes. Do you want to update it with the new dev-tools configuration?', + $file, + ); + + $confirmation = new ConfirmationQuestion($confirmationMessage, false); + + return $this->io->askQuestion($confirmation); + } + + /** + * Builds the managed composer.json payload. + * + * @param string $currentContents the current composer.json file contents + * @param string $file the path being updated, used to resolve local README checks + * + * @return string the composer.json payload with managed sections applied + */ + private function updatedComposerJsonContents(string $currentContents, string $file): string + { + $composerJsonData = json_decode($currentContents, true, 512, \JSON_THROW_ON_ERROR); + + if (! \is_array($composerJsonData)) { + $composerJsonData = []; + } + + $scripts = $composerJsonData['scripts'] ?? []; + if (! \is_array($scripts)) { + $scripts = []; + } + + foreach ($this->getScripts() as $name => $command) { + $scripts[$name] = $command; + } + + $composerJsonData['scripts'] = $scripts; + + if ('' === $this->composer->getReadme() && $this->filesystem->exists( + 'README.md', + \dirname($file) + ) && ! isset($composerJsonData['readme'])) { + $composerJsonData['readme'] = 'README.md'; + } + + $extra = $composerJsonData['extra'] ?? []; + if (! \is_array($extra)) { + $extra = []; + } + + $grumphpConfig = DevToolsPathResolver::getPackagePath('grumphp.yml'); + $grumphpExtra = $extra['grumphp'] ?? []; + if (! \is_array($grumphpExtra)) { + $grumphpExtra = []; + } + + $grumphpExtra['config-default-path'] = Path::makeRelative($grumphpConfig, getcwd()); + $extra['grumphp'] = $grumphpExtra; + $composerJsonData['extra'] = $extra; + + return json_encode( + $composerJsonData, + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE + ) . "\n"; } /** @@ -214,7 +265,7 @@ private function shouldUpdateComposerJson(string $file): bool * * @return array the script name to command map */ - private function scripts(): array + private function getScripts(): array { return [ 'dev-tools' => 'dev-tools', diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 85b9b9119..01d739402 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; @@ -31,6 +30,7 @@ use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; @@ -43,8 +43,12 @@ * Handles the generation of API documentation for the project. * This class MUST NOT be extended and SHALL utilize phpDocumentor to accomplish its task. */ -#[AsCommand(name: 'wiki', description: 'Generates API documentation in Markdown format.')] -final class WikiCommand extends BaseCommand implements LoggerAwareCommandInterface +#[AsCommand( + name: 'github:wiki', + description: 'Generates API documentation in Markdown format.', + aliases: ['.github/wiki', 'wiki'], +)] +final class WikiCommand extends Command { use HasCacheOption; use HasJsonOption; @@ -68,7 +72,7 @@ public function __construct( private readonly GitClientInterface $gitClient, private readonly LoggerInterface $logger, ) { - return parent::__construct(); + parent::__construct(); } /** diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index c048b2344..22d033220 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Filesystem\FinderFactoryInterface; use Psr\Container\ContainerInterface; use ReflectionClass; +use RuntimeException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -96,7 +97,29 @@ private function getCommandMap(FinderFactoryInterface $finderFactory): array } $arguments = $attribute->getArguments(); - $commandMap[$arguments['name']] = $class; + $commandName = $arguments['name'] ?? $arguments[0] ?? ''; + $aliases = $arguments['aliases'] ?? $arguments[2] ?? []; + $commandNames = [$commandName, ...((array) $aliases)]; + + foreach ($commandNames as $commandName) { + if (! \is_string($commandName)) { + continue; + } + + if ('' === $commandName) { + continue; + } + + if (\array_key_exists($commandName, $commandMap) && $commandMap[$commandName] !== $class) { + throw new RuntimeException(\sprintf( + 'Command %s is already registered and cannot be assigned to %s.', + $commandName, + $class + )); + } + + $commandMap[$commandName] = $class; + } } return $commandMap; diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 986836ddd..1e2c20668 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -19,12 +19,10 @@ namespace FastForward\DevTools\Console; -use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use Override; -use Composer\Console\Application as ComposerApplication; +use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use DI\Container; use Psr\Container\ContainerInterface; -use ReflectionMethod; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -32,8 +30,16 @@ * Wraps the fast-forward console tooling suite conceptually as an isolated application instance. * Extending the base application, it MUST provide default command injections safely. */ -final class DevTools extends ComposerApplication +final class DevTools extends Application { + private const string LOGO = <<<'LOGO' + ____ _____ _ + | _ \ _____ _|_ _|__ ___ | |___ + | | | |/ _ \ \ / / | |/ _ \ / _ \| / __| + | |_| | __/\ V / | | (_) | (_) | \__ \ + |____/ \___| \_/ |_|\___/ \___/|_|___/ + LOGO; + /** * @var ContainerInterface holds the static container instance for global access within the DevTools context */ @@ -51,10 +57,21 @@ public function __construct(CommandLoaderInterface $commandLoader) { parent::__construct('Fast Forward Dev Tools'); - $this->setDefaultCommand('standards'); + $this->setDefaultCommand('dev-tools:standards'); $this->setCommandLoader($commandLoader); } + /** + * Gets the help message for the DevTools application, including the ASCII logo. + * + * @return string + */ + #[Override] + public function getHelp(): string + { + return self::LOGO . "\n\n" . parent::getHelp(); + } + /** * Create DevTools instance from container. * @@ -77,20 +94,4 @@ public static function getContainer(): ContainerInterface return self::$container; } - - /** - * Retrieves the default set of commands provided by the Symfony Application. - * - * The method SHOULD NOT add composer-specific commands to the list, - * as they are handled separately by composer when loaded as a plugin. - * - * @return array - */ - #[Override] - protected function getDefaultCommands(): array - { - $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); - - return $reflectionMethod->invoke($this); - } } diff --git a/src/Console/Output/OutputCapabilityDetector.php b/src/Console/Output/OutputCapabilityDetector.php index 95f0dffa4..0622edd3e 100644 --- a/src/Console/Output/OutputCapabilityDetector.php +++ b/src/Console/Output/OutputCapabilityDetector.php @@ -21,6 +21,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; +use Throwable; + +use function Safe\stream_isatty; /** * Detects ANSI-friendly output by decoration state or TTY-backed streams. @@ -44,6 +47,10 @@ public function supportsAnsi(OutputInterface $output): bool return false; } - return stream_isatty($output->getStream()); + try { + return stream_isatty($output->getStream()); + } catch (Throwable) { + return false; + } } } diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index f96217f31..8f22a2bb9 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -197,7 +197,7 @@ public function makePathRelative(string $path, ?string $basePath = null): string * * @return string the base name of the given path */ - public function basename(string $path, string $suffix = ''): string + public function getBasename(string $path, string $suffix = ''): string { return basename($path, $suffix); } @@ -210,7 +210,7 @@ public function basename(string $path, string $suffix = ''): string * * @return string the parent path name */ - public function dirname(string $path, int $levels = 1): string + public function getDirectory(string $path, int $levels = 1): string { return \dirname($path, $levels); } diff --git a/src/Filesystem/FilesystemInterface.php b/src/Filesystem/FilesystemInterface.php index a58d6b83e..74a213521 100644 --- a/src/Filesystem/FilesystemInterface.php +++ b/src/Filesystem/FilesystemInterface.php @@ -140,7 +140,7 @@ public function makePathRelative(string $path, ?string $basePath = null): string * * @return string the base name of the given path */ - public function basename(string $path, string $suffix = ''): string; + public function getBasename(string $path, string $suffix = ''): string; /** * Returns a parent directory's path. @@ -150,5 +150,5 @@ public function basename(string $path, string $suffix = ''): string; * * @return string the parent path name */ - public function dirname(string $path, int $levels = 1): string; + public function getDirectory(string $path, int $levels = 1): string; } diff --git a/src/Funding/ComposerFundingCodec.php b/src/Funding/ComposerFundingCodec.php index 59eeb4790..c9e62e3cf 100644 --- a/src/Funding/ComposerFundingCodec.php +++ b/src/Funding/ComposerFundingCodec.php @@ -19,12 +19,11 @@ namespace FastForward\DevTools\Funding; -use Composer\Json\JsonFile; - -use function Safe\json_encode; use function Safe\parse_url; use function Safe\preg_match; use function array_values; +use function Safe\json_decode; +use function Safe\json_encode; use function trim; /** @@ -41,7 +40,7 @@ */ public function parse(string $contents): FundingProfile { - $data = JsonFile::parseJson($contents); + $data = $this->decodeJsonContents($contents); $funding = $data['funding'] ?? []; if (! \is_array($funding)) { @@ -120,7 +119,7 @@ public function dump(string $contents, FundingProfile $profile): string $entries[] = $unsupportedEntry; } - $data = JsonFile::parseJson($contents); + $data = $this->decodeJsonContents($contents); unset($data['funding']); if ([] === $entries) { @@ -188,4 +187,18 @@ private function insertFundingEntries(array $data, array $entries): array return $orderedData; } + + /** + * Decodes a Composer JSON payload to an array. + * + * @param string $contents the JSON source + * + * @return array the decoded payload + */ + private function decodeJsonContents(string $contents): array + { + $data = json_decode($contents, true, 512, \JSON_THROW_ON_ERROR); + + return \is_array($data) ? $data : []; + } } diff --git a/src/Path/DevToolsPathResolver.php b/src/Path/DevToolsPathResolver.php index 23778ed47..14f2f89a3 100644 --- a/src/Path/DevToolsPathResolver.php +++ b/src/Path/DevToolsPathResolver.php @@ -67,6 +67,18 @@ public static function getBinaryPath(): string return self::getPackagePath(self::BINARY); } + /** + * Returns the packaged DevTools binary command with a subcommand. + * + * @param string $command the DevTools subcommand to append + */ + public static function getBinaryCommand(string $command): string + { + $binaryPath = self::getPackagePath(self::BINARY); + + return \sprintf('%s %s', $binaryPath, $command); + } + /** * Returns the packaged resources directory or a path under it. * diff --git a/src/Path/WorkingProjectPathResolver.php b/src/Path/WorkingProjectPathResolver.php index df013c6fb..cd4258941 100644 --- a/src/Path/WorkingProjectPathResolver.php +++ b/src/Path/WorkingProjectPathResolver.php @@ -29,6 +29,23 @@ */ final class WorkingProjectPathResolver { + /** + * @var list repository-local directories ignored by tooling + */ + public const array TOOLING_EXCLUDED_DIRECTORIES = [ + '.dev-tools', + 'backup', + 'cache', + 'public', + 'resources', + 'tmp', + 'vendor', + '*/vendor', + '*/vendor/*', + '**/vendor', + '**/vendor/*', + ]; + /** * Returns the current working project directory or a path under it. * @@ -52,19 +69,13 @@ public static function getProjectPath(string $path = ''): string */ public static function getToolingExcludedDirectories(string $baseDir = ''): array { - return [ - ManagedWorkspace::getOutputDirectory(baseDir: $baseDir), - Path::join($baseDir, 'backup'), - Path::join($baseDir, 'cache'), - Path::join($baseDir, 'public'), - Path::join($baseDir, 'resources'), - Path::join($baseDir, 'tmp'), - Path::join($baseDir, 'vendor'), - Path::join($baseDir, '*/vendor'), - Path::join($baseDir, '*/vendor/*'), - Path::join($baseDir, '**/vendor'), - Path::join($baseDir, '**/vendor/*'), - ]; + $directories = []; + + foreach (self::TOOLING_EXCLUDED_DIRECTORIES as $excludedDirectory) { + $directories[] = Path::join($baseDir, $excludedDirectory); + } + + return $directories; } /** @@ -81,7 +92,7 @@ public static function getToolingSourcePaths(string $baseDir = ''): array ->files() ->name('*.php') ->in($workingDirectory) - ->exclude(['.dev-tools', 'backup', 'cache', 'public', 'resources', 'tmp', 'vendor']) + ->exclude(self::TOOLING_EXCLUDED_DIRECTORIES) ->sortByName(); $paths = []; diff --git a/src/Process/ColorPreservingProcessEnvironmentConfigurator.php b/src/Process/ColorPreservingProcessEnvironmentConfigurator.php index ca833672c..a93d02ba5 100644 --- a/src/Process/ColorPreservingProcessEnvironmentConfigurator.php +++ b/src/Process/ColorPreservingProcessEnvironmentConfigurator.php @@ -81,6 +81,7 @@ private function shouldForceColor(OutputInterface $output): bool if ($this->outputCapabilityDetector->supportsAnsi($output)) { return true; } + if ($this->isTruthyEnvironmentFlag('FORCE_COLOR')) { return true; } diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index 4dbf8017c..caba841fe 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -96,8 +96,11 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Twig\Loader\FilesystemLoader; use Twig\Loader\LoaderInterface; @@ -157,6 +160,8 @@ public function getFactories(): array ClockInterface::class => get(SystemClock::class), // Console + InputInterface::class => get(ArgvInput::class), + OutputInterface::class => get(ConsoleOutputInterface::class), CommandLoaderInterface::class => get(DevToolsCommandLoader::class), CommandProvider::class => get(DevToolsCommandProvider::class), ConsoleOutputInterface::class => create(ConsoleOutput::class) diff --git a/src/Sync/PackagedDirectorySynchronizer.php b/src/Sync/PackagedDirectorySynchronizer.php index 521231e2c..733af80fc 100644 --- a/src/Sync/PackagedDirectorySynchronizer.php +++ b/src/Sync/PackagedDirectorySynchronizer.php @@ -21,14 +21,13 @@ use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Path; /** * Synchronizes one packaged directory of symlinked entries into a consumer repository. */ -final class PackagedDirectorySynchronizer implements LoggerAwareInterface +final readonly class PackagedDirectorySynchronizer { /** * Initializes the synchronizer with a filesystem and finder factory. @@ -38,19 +37,11 @@ final class PackagedDirectorySynchronizer implements LoggerAwareInterface * @param LoggerInterface $logger Logger for recording synchronization actions and decisions */ public function __construct( - private readonly FilesystemInterface $filesystem, - private readonly FinderFactoryInterface $finderFactory, + private FilesystemInterface $filesystem, + private FinderFactoryInterface $finderFactory, private LoggerInterface $logger, ) {} - /** - * {@inheritDoc} - */ - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - /** * Synchronizes packaged directory entries into the consumer repository. * @@ -141,7 +132,7 @@ private function createNewLink( SynchronizeResult $result, ): void { $relativeSourcePath = $this->normalizeRelativeSourcePath( - $this->filesystem->makePathRelative($sourcePath, $this->filesystem->dirname($targetLink)), + $this->filesystem->makePathRelative($sourcePath, $this->filesystem->getDirectory($targetLink)), $isDirectory, ); diff --git a/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php b/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php index ed8b066c4..aeac837e7 100644 --- a/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php +++ b/tests/Changelog/Checker/UnreleasedEntryCheckerTest.php @@ -71,7 +71,7 @@ protected function setUp(): void $this->gitClient = $this->prophesize(GitClientInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->parser = $this->prophesize(ChangelogParserInterface::class); - $this->filesystem->dirname(self::FILE) + $this->filesystem->getDirectory(self::FILE) ->willReturn(self::WORKING_DIRECTORY); $this->checker = new UnreleasedEntryChecker( $this->filesystem->reveal(), diff --git a/tests/Changelog/Manager/ChangelogManagerTest.php b/tests/Changelog/Manager/ChangelogManagerTest.php index 3ec95bff3..e6527031a 100644 --- a/tests/Changelog/Manager/ChangelogManagerTest.php +++ b/tests/Changelog/Manager/ChangelogManagerTest.php @@ -85,7 +85,7 @@ protected function setUp(): void $this->renderer->reveal(), $this->gitClient->reveal(), ); - $this->filesystem->dirname(self::FILE) + $this->filesystem->getDirectory(self::FILE) ->willReturn(self::WORKING_DIRECTORY); } diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 5bddbda5a..6f5a5a9c7 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -19,8 +19,9 @@ namespace FastForward\DevTools\Tests\Composer\Capability; -use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; +use FastForward\DevTools\Composer\Command\ProxyCommand; +use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; use FastForward\DevTools\Console\DevTools; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -30,10 +31,10 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use ReflectionProperty; -use Symfony\Component\Console\Command\Command; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(DevTools::class)] +#[UsesClass(ProxyCommand::class)] final class DevToolsCommandProviderTest extends TestCase { use ProphecyTrait; @@ -81,18 +82,80 @@ public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): vo * @return void */ #[Test] - public function getCommandsWillReturnRegisteredBaseCommands(): void + public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCommands(): void { - $composerCommand = $this->prophesize(BaseCommand::class)->reveal(); - $symfonyCommand = $this->prophesize(Command::class)->reveal(); + $symfonyCommand = new FixtureWithoutAsCommand('agents'); + $symfonyCommand->setAliases([]); + $symfonyCommand->setDescription('Synchronize agents.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); $this->devTools->all() - ->willReturn([$composerCommand, $symfonyCommand])->shouldBeCalledOnce(); + ->willReturn([ + 'agents' => $symfonyCommand, + ]) + ->shouldBeCalledOnce(); - $commands = $this->commandProvider->getCommands(); + $commands = array_values($this->commandProvider->getCommands()); + $command = $commands[0]; self::assertIsArray($commands); self::assertCount(1, $commands); - self::assertSame($composerCommand, $commands[0]); + self::assertInstanceOf(ProxyCommand::class, $command); + self::assertSame('agents', $command->getName()); + } + + /** + * @return void + */ + #[Test] + public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): void + { + $symfonyCommand = new FixtureWithoutAsCommand('reports:tests'); + $symfonyCommand->setAliases(['tests', 'phpunit']); + $symfonyCommand->setDescription('Runs PHPUnit tests.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); + + $this->devTools->all() + ->willReturn([ + 'reports:tests' => $symfonyCommand, + 'tests' => $symfonyCommand, + ]) + ->shouldBeCalledOnce(); + + $commands = array_values($this->commandProvider->getCommands()); + $proxyCommand = $commands[0]; + + self::assertCount(1, $commands); + self::assertInstanceOf(ProxyCommand::class, $proxyCommand); + self::assertSame('reports:tests', $proxyCommand->getName()); + self::assertSame(['tests', 'phpunit'], $proxyCommand->getAliases()); + } + + /** + * @return void + */ + #[Test] + public function getCommandsWillPreserveAliasDefinitionsInProxyCommand(): void + { + $symfonyCommand = new FixtureWithoutAsCommand('dev-tools:standards'); + $symfonyCommand->setAliases(['standards']); + $symfonyCommand->setDescription('Runs standards checks.'); + $symfonyCommand->setHelp(''); + $symfonyCommand->setHidden(false); + + $this->devTools->all() + ->willReturn([ + 'dev-tools:standards' => $symfonyCommand, + 'standards' => $symfonyCommand, + ]) + ->shouldBeCalledOnce(); + + $proxyCommand = array_values($this->commandProvider->getCommands())[0]; + + self::assertInstanceOf(ProxyCommand::class, $proxyCommand); + self::assertSame('dev-tools:standards', $proxyCommand->getName()); + self::assertSame(['standards'], $proxyCommand->getAliases()); } } diff --git a/tests/Composer/Json/ComposerJsonTest.php b/tests/Composer/Json/ComposerJsonTest.php index 7719a7517..00335569a 100644 --- a/tests/Composer/Json/ComposerJsonTest.php +++ b/tests/Composer/Json/ComposerJsonTest.php @@ -26,6 +26,7 @@ use FastForward\DevTools\Composer\Json\Schema\Funding; use FastForward\DevTools\Composer\Json\Schema\Support; use FastForward\DevTools\Composer\Json\Schema\SupportInterface; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -41,6 +42,7 @@ #[UsesClass(Author::class)] #[UsesClass(Funding::class)] #[UsesClass(Support::class)] +#[UsesClass(WorkingProjectPathResolver::class)] final class ComposerJsonTest extends TestCase { /** diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index edfc67db9..8c6de41d3 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\Console\Application; -use Composer\IO\IOInterface; use FastForward\DevTools\Console\Command\AgentsCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -37,7 +35,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use ReflectionMethod; -use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -62,10 +59,6 @@ final class AgentsCommandTest extends TestCase private ObjectProphecy $output; - private ObjectProphecy $application; - - private ObjectProphecy $io; - private AgentsCommand $command; /** @@ -78,13 +71,6 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); - $this->application = $this->prophesize(Application::class); - $this->io = $this->prophesize(IOInterface::class); - - $this->application->getHelperSet() - ->willReturn(new HelperSet()); - $this->application->getIO() - ->willReturn($this->io->reveal()); $this->filesystem->getAbsolutePath('.agents/agents') ->willReturn(getcwd() . '/.agents/agents'); @@ -93,7 +79,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->logger->reveal(), ); - $this->command->setApplication($this->application->reveal()); } /** @@ -106,7 +91,6 @@ public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void $this->filesystem->exists($agentsPath) ->willReturn(false); - $this->synchronizer->setLogger(Argument::cetera())->shouldNotBeCalled(); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); $this->logger->info('Starting agents synchronization...') ->shouldBeCalledOnce(); @@ -138,8 +122,6 @@ public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void ->willReturn(true, false); $this->filesystem->mkdir($agentsPath) ->shouldBeCalledOnce(); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); @@ -173,8 +155,6 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->filesystem->exists($agentsPath) ->willReturn(true, true); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index b809eb956..d2e1b7b08 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\CodeOwners\CodeOwnersGenerator; use FastForward\DevTools\Console\Command\CodeOwnersCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; @@ -93,7 +94,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->input->getOption('file') ->willReturn('.github/CODEOWNERS'); @@ -121,8 +122,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -131,7 +132,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('codeowners', $this->command->getName()); + self::assertSame('github:codeowners', $this->command->getName()); self::assertSame( 'Generates .github/CODEOWNERS from local project metadata.', $this->command->getDescription(), @@ -154,7 +155,7 @@ public function executeWillWriteGeneratedCodeOwnersWhenFileIsMissing(): void $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn($targetDirectory); $this->filesystem->exists($targetPath) ->willReturn(false, false); @@ -192,7 +193,7 @@ public function executeWillSkipExistingCodeOwnersByDefault(): void $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -226,7 +227,7 @@ public function executeWillFailCheckModeWhenDriftIsDetected(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -268,7 +269,7 @@ public function executeWillPromptForOwnersWhenInteractiveInferenceFails(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn($targetDirectory); $this->filesystem->exists($targetPath) ->willReturn(false, false); @@ -278,7 +279,6 @@ public function executeWillPromptForOwnersWhenInteractiveInferenceFails(): void ->willReturn([]); $this->io->ask( 'No CODEOWNERS entries could be inferred from composer.json. Enter space-separated owners for "*" or leave blank to use a commented placeholder: ', - '', )->willReturn('php-fast-forward @mentordosnerds') ->shouldBeCalledOnce(); $this->generator->normalizeOwners('php-fast-forward @mentordosnerds') @@ -318,7 +318,7 @@ public function executeWillReturnSuccessOnDryRunWhenDriftIsDetected(): void ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -359,7 +359,7 @@ public function executeWillSkipReplacingExistingCodeOwnersWhenConfirmationIsDecl ->willReturn(true); $this->filesystem->getAbsolutePath('.github/CODEOWNERS') ->willReturn($targetPath); - $this->filesystem->dirname($targetPath) + $this->filesystem->getDirectory($targetPath) ->willReturn('/project/.github'); $this->filesystem->exists($targetPath) ->willReturn(true); @@ -379,7 +379,7 @@ public function executeWillSkipReplacingExistingCodeOwnersWhenConfirmationIsDecl FileDiff::STATUS_CHANGED, 'Updating managed file ' . $targetPath . ' from generated CODEOWNERS content.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index 5cd06a132..df7a3fda1 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\CopyResourceCommand; use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -86,7 +87,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->output->writeln(Argument::any()); @@ -109,8 +110,8 @@ protected function setUp(): void $this->finderFactory->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -131,7 +132,7 @@ protected function tearDown(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('copy-resource', $this->command->getName()); + self::assertSame('dev-tools:sync:copy', $this->command->getName()); self::assertSame( 'Copies a file or directory resource into the current project.', $this->command->getDescription() @@ -437,7 +438,7 @@ public function executeWillSkipReplacingDriftedFileWhenInteractiveConfirmationIs $this->fileDiffer->diff('/package/.editorconfig', '/project/.editorconfig') ->willReturn(new FileDiff(FileDiff::STATUS_CHANGED, 'Changed summary', "@@ -1 +1 @@\n-old\n+new")) ->shouldBeCalledOnce(); - $this->io->askConfirmation('Replace drifted resource /project/.editorconfig? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log('notice', 'Skipped replacing {target_path}.', Argument::type('array')) diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index c7810af63..47e658440 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\FundingCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -91,7 +92,7 @@ protected function setUp(): void $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->normalizeProcess = $this->prophesize(Process::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->logger = $this->prophesize(LoggerInterface::class); $this->output->isDecorated() ->willReturn(false); @@ -111,11 +112,11 @@ protected function setUp(): void ->willReturn(false); $this->input->getOption('interactive') ->willReturn(false); - $this->filesystem->dirname('.github/FUNDING.yml') + $this->filesystem->getDirectory('.github/FUNDING.yml') ->willReturn('.github'); - $this->filesystem->dirname('composer.json') + $this->filesystem->getDirectory('composer.json') ->willReturn('.'); - $this->filesystem->basename('composer.json') + $this->filesystem->getBasename('composer.json') ->willReturn('composer.json'); $this->processBuilder->withArgument(Argument::any())->willReturn($this->processBuilder->reveal()); $this->processBuilder->withArgument(Argument::any(), Argument::any())->willReturn( @@ -133,8 +134,8 @@ protected function setUp(): void $this->processBuilder->reveal(), $this->processQueue->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -466,7 +467,7 @@ public function executeWillSkipComposerWriteWhenInteractiveConfirmationIsDecline ->willReturn(true); $this->input->isInteractive() ->willReturn(true); - $this->io->askConfirmation('Update managed file composer.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->filesystem->exists('composer.json') @@ -572,9 +573,9 @@ public function executeWillPassWorkingDirectoryAndAlternateManifestToComposerNor ->willReturn(true); $this->filesystem->readFile('.github/FUNDING.yml') ->willReturn($fundingYaml); - $this->filesystem->dirname($composerFile) + $this->filesystem->getDirectory($composerFile) ->willReturn('build/custom'); - $this->filesystem->basename($composerFile) + $this->filesystem->getBasename($composerFile) ->willReturn('composer.alt.json'); $this->processBuilder->withArgument('--working-dir', 'build/custom') ->willReturn($this->processBuilder->reveal()) @@ -614,7 +615,7 @@ public function executeWillPassWorkingDirectoryAndAlternateManifestToComposerNor #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('funding', $this->command->getName()); + self::assertSame('github:funding', $this->command->getName()); self::assertSame( 'Synchronizes funding metadata between composer.json and .github/FUNDING.yml.', $this->command->getDescription(), @@ -672,12 +673,12 @@ public function privateHelpersWillPromptAndNormalizeComposerFileArguments(): voi $shouldWriteManagedFile = new ReflectionMethod($this->command, 'shouldWriteManagedFile'); $normalizeComposerFile = new ReflectionMethod($this->command, 'normalizeComposerFile'); - $this->io->askConfirmation('Update managed file composer.alt.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(true) ->shouldBeCalledOnce(); - $this->filesystem->dirname('composer.alt.json') + $this->filesystem->getDirectory('composer.alt.json') ->willReturn('.'); - $this->filesystem->basename('composer.alt.json') + $this->filesystem->getBasename('composer.alt.json') ->willReturn('composer.alt.json'); $this->processBuilder->withArgument('--file', 'composer.alt.json') ->willReturn($this->processBuilder->reveal()) diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index 7c186b350..f25f85a21 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -19,8 +19,9 @@ namespace FastForward\DevTools\Tests\Console\Command; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Prophecy\Argument; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\GitAttributesCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; @@ -135,7 +136,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->output->writeln(Argument::any()); @@ -168,8 +169,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -178,7 +179,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('gitattributes', $this->command->getName()); + self::assertSame('git:attributes', $this->command->getName()); self::assertSame( 'Manages .gitattributes export-ignore rules for leaner package archives.', $this->command->getDescription() @@ -484,7 +485,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'Managed file needs update.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Update managed file %s? [y/N] ', $gitattributesPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice('Skipped updating {gitattributes_path}.', Argument::type('array')) diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index c450b9f6e..beb38ce59 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -19,8 +19,9 @@ namespace FastForward\DevTools\Tests\Console\Command; +use Symfony\Component\Console\Question\ConfirmationQuestion; use FastForward\DevTools\Resource\FileDiff; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\GitHooksCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FinderFactoryInterface; @@ -90,7 +91,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -114,8 +115,8 @@ protected function setUp(): void $this->finderFactory->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -135,7 +136,7 @@ protected function tearDown(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('git-hooks', $this->command->getName()); + self::assertSame('git:hooks', $this->command->getName()); self::assertSame('Installs Fast Forward Git hooks.', $this->command->getDescription()); self::assertSame( 'This command copies packaged Git hooks into the current repository.', @@ -309,7 +310,7 @@ public function executeWillSkipReplacingHookWhenInteractiveConfirmationIsDecline $this->fileDiffer->formatForConsole("@@ -1 +1 @@\n-old\n+new", false) ->willReturn("@@ -1 +1 @@\n-old\n+new") ->shouldBeCalledOnce(); - $this->io->askConfirmation('Replace drifted Git hook /app/.git/hooks/post-merge? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice( @@ -399,7 +400,7 @@ public function executeWillReportInstallFailureWhenReplacementStillCannotBeWritt new IOException('Target file could not be opened for writing.', 0, null, '/app/.git/hooks/post-merge') ) ->shouldBeCalledOnce(); - $this->filesystem->basename('/app/.git/hooks/post-merge') + $this->filesystem->getBasename('/app/.git/hooks/post-merge') ->willReturn('post-merge') ->shouldBeCalledOnce(); $this->logger->error( diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index f1787d48a..4bebec70a 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\GitIgnoreCommand; use FastForward\DevTools\GitIgnore\GitIgnoreInterface; use FastForward\DevTools\GitIgnore\MergerInterface; @@ -124,7 +125,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -181,8 +182,8 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -191,7 +192,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('gitignore', $this->command->getName()); + self::assertSame('git:ignore', $this->command->getName()); self::assertSame('Merges and synchronizes .gitignore files.', $this->command->getDescription()); self::assertSame( "This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore.", @@ -299,7 +300,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v ->willReturn(true); $this->input->isInteractive() ->willReturn(true); - $this->io->askConfirmation(\sprintf('Update managed file %s? [y/N] ', self::TARGET_PATH), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index b14edd534..598c680bc 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\LicenseCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Output\GithubActionOutput; @@ -98,7 +99,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->input->getOption('dry-run') @@ -121,8 +122,8 @@ protected function setUp(): void $this->filesystem->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -131,7 +132,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('license', $this->command->getName()); + self::assertSame('license:generate', $this->command->getName()); self::assertSame( 'Generates a LICENSE file from composer.json license information.', $this->command->getDescription() @@ -370,7 +371,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'Updating managed file ' . $targetPath . ' from generated LICENSE content.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation(\sprintf('Write managed file %s? [y/N] ', $targetPath), false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->notice( diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index b7b5775fb..1737dde21 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -73,7 +73,7 @@ protected function setUp(): void $this->process = $this->prophesize(Process::class); $this->input->getOption('exclude') - ->willReturn('vendor,tests/Fixtures'); + ->willReturn('vendor'); $this->input->getOption('target') ->willReturn(ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS)); $this->input->getOption('junit') diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 9d1576b59..6f09cbedf 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -25,6 +25,7 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +43,7 @@ #[CoversClass(ReportsCommand::class)] #[UsesClass(ManagedWorkspace::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesTrait(LogsCommandResults::class)] final class ReportsCommandTest extends TestCase { diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php index 4825ee1ba..d2e72d79c 100644 --- a/tests/Console/Command/SkillsCommandTest.php +++ b/tests/Console/Command/SkillsCommandTest.php @@ -19,8 +19,6 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\Console\Application; -use Composer\IO\IOInterface; use FastForward\DevTools\Console\Command\SkillsCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -37,7 +35,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; use ReflectionMethod; -use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -62,10 +59,6 @@ final class SkillsCommandTest extends TestCase private ObjectProphecy $output; - private ObjectProphecy $application; - - private ObjectProphecy $io; - private SkillsCommand $command; /** @@ -78,13 +71,6 @@ protected function setUp(): void $this->logger = $this->prophesize(LoggerInterface::class); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); - $this->application = $this->prophesize(Application::class); - $this->io = $this->prophesize(IOInterface::class); - - $this->application->getHelperSet() - ->willReturn(new HelperSet()); - $this->application->getIO() - ->willReturn($this->io->reveal()); $this->filesystem->getAbsolutePath('.agents/skills') ->willReturn(getcwd() . '/.agents/skills'); @@ -93,7 +79,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->logger->reveal(), ); - $this->command->setApplication($this->application->reveal()); } /** @@ -106,7 +91,6 @@ public function executeWillFailWhenPackagedSkillsDirectoryDoesNotExist(): void $this->filesystem->exists($skillsPath) ->willReturn(false); - $this->synchronizer->setLogger(Argument::cetera())->shouldNotBeCalled(); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); $this->logger->info('Starting skills synchronization...') ->shouldBeCalledOnce(); @@ -138,8 +122,6 @@ public function executeWillCreateSkillsDirectoryWhenItDoesNotExist(): void ->willReturn(true, false); $this->filesystem->mkdir($skillsPath) ->shouldBeCalledOnce(); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); @@ -173,8 +155,6 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->filesystem->exists($skillsPath) ->willReturn(true, true); - $this->synchronizer->setLogger($this->io->reveal()) - ->shouldBeCalledOnce(); $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index e7400b197..9ebab4d62 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -35,11 +35,13 @@ use Psr\Log\LoggerInterface; use ReflectionMethod; use FastForward\DevTools\Path\ManagedWorkspace; +use FastForward\DevTools\Path\DevToolsPathResolver; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[CoversClass(StandardsCommand::class)] #[UsesClass(ManagedWorkspace::class)] +#[UsesClass(DevToolsPathResolver::class)] #[UsesTrait(LogsCommandResults::class)] final class StandardsCommandTest extends TestCase { diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index 85a09b560..5427826ec 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -19,7 +19,8 @@ namespace FastForward\DevTools\Tests\Console\Command; -use Composer\IO\IOInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\UpdateComposerJsonCommand; use FastForward\DevTools\Filesystem\FilesystemInterface; @@ -78,7 +79,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); - $this->io = $this->prophesize(IOInterface::class); + $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) @@ -102,8 +103,8 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->fileDiffer->reveal(), $this->logger->reveal(), + $this->io->reveal(), ); - $this->command->setIO($this->io->reveal()); } /** @@ -112,7 +113,7 @@ protected function setUp(): void #[Test] public function commandWillSetExpectedNameDescriptionAndHelp(): void { - self::assertSame('update-composer-json', $this->command->getName()); + self::assertSame('dev-tools:sync:composer', $this->command->getName()); self::assertSame( 'Updates composer.json with Fast Forward dev-tools scripts and metadata.', $this->command->getDescription() @@ -432,7 +433,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v FileDiff::STATUS_CHANGED, 'composer.json must be updated.', ))->shouldBeCalledOnce(); - $this->io->askConfirmation('Update managed file /app/composer.json? [y/N] ', false) + $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); $this->logger->log( diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php index 7a5ac27c3..9d8d26f59 100644 --- a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -20,9 +20,12 @@ namespace FastForward\DevTools\Tests\Console\CommandLoader; use ArrayIterator; -use FastForward\DevTools\Console\Command\CodeStyleCommand; +use FastForward\DevTools\Console\Command\AgentsCommand; +use FastForward\DevTools\Console\Command\SyncCommand; +use FastForward\DevTools\Console\Command\TestsCommand; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use FastForward\DevTools\Filesystem\FinderFactoryInterface; +use RuntimeException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -36,7 +39,8 @@ use Symfony\Component\Finder\SplFileInfo; #[CoversClass(DevToolsCommandLoader::class)] -#[UsesClass(CodeStyleCommand::class)] +#[UsesClass(AgentsCommand::class)] +#[UsesClass(SyncCommand::class)] final class DevToolsCommandLoaderTest extends TestCase { use ProphecyTrait; @@ -90,16 +94,123 @@ public function constructorWillRegisterOnlyInstantiableCommands(): void ->shouldBeCalled(); $this->finder->getIterator() ->willReturn(new ArrayIterator([ - new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'), + new SplFileInfo($commandDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), ]))->shouldBeCalled(); - $this->container->has(CodeStyleCommand::class)->willReturn(true)->shouldBeCalled(); - $this->container->get(CodeStyleCommand::class)->willReturn($command->reveal())->shouldBeCalled(); + $this->container->has(AgentsCommand::class)->willReturn(true)->shouldBeCalled(); + $this->container->get(AgentsCommand::class)->willReturn($command->reveal())->shouldBeCalled(); $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); - self::assertTrue($loader->has('code-style')); - self::assertSame($command->reveal(), $loader->get('code-style')); + self::assertTrue($loader->has('agents')); + self::assertSame($command->reveal(), $loader->get('agents')); + } + + /** + * @return void + */ + #[Test] + public function constructorWillRegisterPrimaryCommandFromAsCommandAttribute(): void + { + $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo($commandDirectory . '/SyncCommand.php', '', 'SyncCommand.php'), + ]))->shouldBeCalled(); + $this->container->has(SyncCommand::class)->willReturn(true)->shouldBeCalled(); + + $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); + + self::assertTrue($loader->has('dev-tools:sync')); + } + + /** + * @return void + */ + #[Test] + public function constructorWillRegisterAliasesFromAsCommandAttribute(): void + { + $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo($commandDirectory . '/TestsCommand.php', '', 'TestsCommand.php'), + ]))->shouldBeCalled(); + $this->container->has(TestsCommand::class)->willReturn(true)->shouldBeCalled(); + + $loader = new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); + + self::assertTrue($loader->has('reports:tests')); + self::assertTrue($loader->has('tests')); + self::assertTrue($loader->has('phpunit')); + } + + /** + * @return void + */ + #[Test] + public function constructorWillFailWhenCommandNameConflicts(): void + { + $commandDirectory = \dirname(__DIR__, 3); + $fixtureDirectory = $commandDirectory . '/tests/Fixtures/Console/Command'; + $srcDirectory = $commandDirectory . '/src/Console/Command'; + + $this->finderFactory->create() + ->willReturn($this->finder->reveal()) + ->shouldBeCalledOnce(); + $this->finder->files() + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->in(Argument::type('string'))->willReturn($this->finder->reveal())->shouldBeCalled(); + $this->finder->notPath('Traits') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->name('*.php') + ->willReturn($this->finder->reveal()) + ->shouldBeCalled(); + $this->finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo( + $fixtureDirectory . '/FixtureDuplicateCommandName.php', + '', + 'FixtureDuplicateCommandName.php' + ), + new SplFileInfo($srcDirectory . '/AgentsCommand.php', '', 'AgentsCommand.php'), + ]))->shouldBeCalled(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'Command agents is already registered and cannot be assigned to FastForward\\DevTools\\Console\\Command\\AgentsCommand.' + ); + + new DevToolsCommandLoader($this->finderFactory->reveal(), $this->container->reveal()); } /** diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index b9995c9ae..15406e239 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -31,6 +31,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use ReflectionMethod; use ReflectionProperty; @@ -67,6 +68,8 @@ protected function setUp(): void $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); $this->commandLoader->getNames() ->willReturn([]); + $this->commandLoader->has(Argument::type('string')) + ->willReturn(false); $this->devTools = new DevTools($this->commandLoader->reveal()); } @@ -110,6 +113,48 @@ public function __construct() self::assertSame($customCommand, $this->devTools->get('custom')); } + /** + * @return void + */ + #[Test] + public function allWillReturnLoaderCommandsWithPreservedKeys(): void + { + $commands = [ + 'agents' => new class extends Command { + public function __construct() + { + parent::__construct('agents'); + } + }, + 'sync' => new class extends Command { + public function __construct() + { + parent::__construct('sync'); + } + }, + ]; + + $this->commandLoader->getNames() + ->willReturn(array_keys($commands)); + $this->commandLoader->has('agents') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->has('sync') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get('agents') + ->willReturn($commands['agents']); + $this->commandLoader->get('sync') + ->willReturn($commands['sync']); + + $providedCommands = $this->devTools->all(); + + self::assertArrayHasKey('agents', $providedCommands); + self::assertArrayHasKey('sync', $providedCommands); + self::assertSame($commands['agents'], $providedCommands['agents']); + self::assertSame($commands['sync'], $providedCommands['sync']); + } + /** * @return void */ diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index 7f315cd8e..70bebd409 100644 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -114,8 +114,8 @@ public function getAbsolutePathWillResolveRelativeBasePathsAgainstCurrentWorking #[Test] public function basenameWillReturnCorrectBasename(): void { - self::assertSame('file', $this->filesystem->basename('/path/to/file.txt', '.txt')); - self::assertSame('file.txt', $this->filesystem->basename('/path/to/file.txt')); + self::assertSame('file', $this->filesystem->getBasename('/path/to/file.txt', '.txt')); + self::assertSame('file.txt', $this->filesystem->getBasename('/path/to/file.txt')); } /** @@ -124,8 +124,8 @@ public function basenameWillReturnCorrectBasename(): void #[Test] public function dirnameWillReturnCorrectDirname(): void { - self::assertSame('/path/to', $this->filesystem->dirname('/path/to/file.txt')); - self::assertSame('/path', $this->filesystem->dirname('/path/to/file.txt', 2)); + self::assertSame('/path/to', $this->filesystem->getDirectory('/path/to/file.txt')); + self::assertSame('/path', $this->filesystem->getDirectory('/path/to/file.txt', 2)); } /** diff --git a/src/Console/Command/LoggerAwareCommandInterface.php b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php similarity index 58% rename from src/Console/Command/LoggerAwareCommandInterface.php rename to tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php index 9010f8385..ae023fd3f 100644 --- a/src/Console/Command/LoggerAwareCommandInterface.php +++ b/tests/Fixtures/Console/Command/FixtureDuplicateCommandName.php @@ -19,18 +19,8 @@ namespace FastForward\DevTools\Console\Command; -use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; -/** - * Defines the logger contract consumed by reusable command result helpers. - * - * Commands that compose shared logging traits expose their logger through this - * interface so the traits can stay agnostic of the concrete command class. - */ -interface LoggerAwareCommandInterface -{ - /** - * Returns the logger used to emit command lifecycle messages. - */ - public function getLogger(): LoggerInterface; -} +#[AsCommand(name: 'agents')] +final class FixtureDuplicateCommandName extends Command {} diff --git a/tests/Fixtures/composer-plugin-consumer/.gitignore b/tests/Fixtures/composer-plugin-consumer/.gitignore deleted file mode 100644 index ed3675727..000000000 --- a/tests/Fixtures/composer-plugin-consumer/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!.gitignore -!composer.json diff --git a/tests/Fixtures/composer-plugin-consumer/composer.json b/tests/Fixtures/composer-plugin-consumer/composer.json deleted file mode 100644 index 765085a09..000000000 --- a/tests/Fixtures/composer-plugin-consumer/composer.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "fast-forward/dev-tools-composer-plugin-consumer-fixture", - "description": "Fixture project used to verify DevTools Composer plugin consumer behavior.", - "license": "MIT", - "type": "project", - "require-dev": { - "fast-forward/dev-tools": "*@dev" - }, - "repositories": [ - { - "type": "path", - "url": "../../..", - "options": { - "symlink": true - } - } - ], - "minimum-stability": "dev", - "prefer-stable": true, - "config": { - "allow-plugins": { - "ergebnis/composer-normalize": true, - "fast-forward/dev-tools": true, - "phpdocumentor/shim": true, - "phpro/grumphp-shim": true, - "pyrech/composer-changelogs": true - } - }, - "extra": { - "grumphp": { - "config-default-path": "../../../grumphp.yml" - } - }, - "scripts": { - "dev-tools": "dev-tools", - "dev-tools:fix": "@dev-tools --fix" - } -} diff --git a/tests/Path/WorkingProjectPathResolverTest.php b/tests/Path/WorkingProjectPathResolverTest.php index c872e5f24..7494048b3 100644 --- a/tests/Path/WorkingProjectPathResolverTest.php +++ b/tests/Path/WorkingProjectPathResolverTest.php @@ -26,11 +26,13 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use function Safe\scandir; +use function Safe\rmdir; +use function Safe\unlink; use function Safe\file_put_contents; use function Safe\getcwd; use function Safe\mkdir; use function Safe\realpath; -use function sys_get_temp_dir; use function uniqid; #[CoversClass(WorkingProjectPathResolver::class)] @@ -113,9 +115,9 @@ public function itWillExposeRelativeToolingSkipPatternsByDefault(): void * @return void */ #[Test] - public function itWillExposeToolingSourcePathsWithoutTraversingVendorDirectories(): void + public function itWillExposeToolingSourcePathsIgnoringExcludedDirectories(): void { - $fixtureDirectory = sys_get_temp_dir() . '/dev-tools-path-resolver-' . uniqid(); + $fixtureDirectory = \dirname(__DIR__, 2) . '/backup/dev-tools-path-resolver-' . uniqid(); mkdir($fixtureDirectory . '/src', recursive: true); mkdir($fixtureDirectory . '/tests/Fixtures/consumer/vendor/package/src', recursive: true); @@ -130,13 +132,55 @@ public function itWillExposeToolingSourcePathsWithoutTraversingVendorDirectories file_put_contents($fixtureDirectory . '/backup/Backup.php', 'logger = $this->prophesize(LoggerInterface::class); } - /** - * @return void - */ - #[Test] - public function setLoggerWillReplaceTheActiveLogger(): void - { - $replacementLogger = $this->prophesize(LoggerInterface::class); - $synchronizer = $this->createSynchronizer(); - - $this->filesystem->exists('/package/.agents/agents') - ->willReturn(false); - - $synchronizer->setLogger($replacementLogger->reveal()); - - $replacementLogger->error('No packaged .agents/agents found at: /package/.agents/agents') - ->shouldBeCalledOnce(); - - $result = $synchronizer - ->synchronize('/consumer/.agents/agents', '/package/.agents/agents', '.agents/agents'); - - self::assertTrue($result->failed()); - } - /** * @return void */ @@ -135,7 +112,7 @@ public function synchronizeWithMissingTargetDirWillCreateItAndCreateLinks(): voi ->shouldBeCalledOnce(); $this->filesystem->exists('/consumer/.agents/agents/issue-editor') ->willReturn(false); - $this->filesystem->dirname('/consumer/.agents/agents/issue-editor') + $this->filesystem->getDirectory('/consumer/.agents/agents/issue-editor') ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') @@ -213,7 +190,7 @@ public function synchronizeWillRepairBrokenSymlink(): void ->willReturn(false); $this->filesystem->remove($targetLink) ->shouldBeCalledOnce(); - $this->filesystem->dirname($targetLink) + $this->filesystem->getDirectory($targetLink) ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') @@ -283,7 +260,7 @@ public function synchronizeWillCreateLinksForTopLevelFiles(): void ->willReturn(true); $this->filesystem->exists($targetLink) ->willReturn(false); - $this->filesystem->dirname($targetLink) + $this->filesystem->getDirectory($targetLink) ->willReturn('/consumer/.agents/agents') ->shouldBeCalledOnce(); $this->filesystem->makePathRelative($entryPath, '/consumer/.agents/agents') @@ -347,21 +324,11 @@ private function createEntry(string $entryName, string $sourcePath, bool $isDire * @return PackagedDirectorySynchronizer */ private function createSynchronizer(): PackagedDirectorySynchronizer - { - return $this->createSynchronizerWithLogger($this->logger->reveal()); - } - - /** - * @param LoggerInterface $logger - * - * @return PackagedDirectorySynchronizer - */ - private function createSynchronizerWithLogger(LoggerInterface $logger): PackagedDirectorySynchronizer { return new PackagedDirectorySynchronizer( $this->filesystem->reveal(), $this->finderFactory->reveal(), - $logger, + $this->logger->reveal(), ); } }