From bcc7f7021a1ee14e345ad15142f29c2ca8e9b470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 14:15:20 -0300 Subject: [PATCH 01/14] refactor: reorganize commands into Console namespace with dependency injection --- bin/dev-tools.php | 3 +- composer.json | 2 + config/container.php | 51 ++++++++++++++ docs/advanced/consumer-automation.rst | 2 +- docs/api/commands.rst | 30 ++++---- docs/configuration/overriding-defaults.rst | 2 +- docs/faq.rst | 4 +- docs/internals/architecture.rst | 12 ++-- src/Agent/Skills/SkillsSynchronizer.php | 24 ++++--- .../Capability/DevToolsCommandProvider.php | 47 ++----------- src/Composer/Json/ComposerJson.php | 59 ++++++++++++++++ src/{ => Console}/Command/AbstractCommand.php | 66 ++---------------- .../Command/CodeStyleCommand.php | 11 +-- .../Command/CopyLicenseCommand.php | 48 ++++--------- .../Command/DependenciesCommand.php | 29 +++----- src/{ => Console}/Command/DocsCommand.php | 24 +++++-- .../Command/GitAttributesCommand.php | 62 ++++++----------- .../Command/GitIgnoreCommand.php | 32 ++++----- src/{ => Console}/Command/PhpDocCommand.php | 32 +++++---- src/{ => Console}/Command/RefactorCommand.php | 12 ++-- src/{ => Console}/Command/ReportsCommand.php | 27 ++------ src/{ => Console}/Command/SkillsCommand.php | 49 ++++--------- .../Command/StandardsCommand.php | 14 ++-- src/{ => Console}/Command/SyncCommand.php | 30 +++----- src/{ => Console}/Command/TestsCommand.php | 22 +++--- src/{ => Console}/Command/WikiCommand.php | 25 +++++-- .../CommandLoader/DevToolsCommandLoader.php | 44 ++++++++++++ src/Console/DevTools.php | 63 +++++++++++++++++ src/DevTools.php | 69 ------------------- src/License/Generator.php | 2 +- src/License/PlaceholderResolver.php | 10 ++- src/License/Reader.php | 68 ++++-------------- src/Psr/Clock/SystemClock.php | 29 ++++++++ src/Psr/Container/Container.php | 35 ++++++++++ tests/Command/AbstractCommandTest.php | 9 +-- tests/Command/AbstractCommandTestCase.php | 3 +- tests/Command/CodeStyleCommandTest.php | 2 +- tests/Command/CopyLicenseCommandTest.php | 44 ++++++++---- tests/Command/DependenciesCommandTest.php | 2 +- tests/Command/DocsCommandTest.php | 28 ++++++-- tests/Command/GitIgnoreCommandTest.php | 6 +- tests/Command/PhpDocCommandTest.php | 4 +- tests/Command/RefactorCommandTest.php | 2 +- tests/Command/ReportsCommandTest.php | 2 +- tests/Command/SkillsCommandTest.php | 2 +- tests/Command/StandardsCommandTest.php | 2 +- tests/Command/SyncCommandTest.php | 4 +- tests/Command/TestsCommandTest.php | 15 +++- tests/Command/WikiCommandTest.php | 2 +- .../DevToolsCommandProviderTest.php | 30 ++++---- 50 files changed, 635 insertions(+), 561 deletions(-) create mode 100644 config/container.php create mode 100644 src/Composer/Json/ComposerJson.php rename src/{ => Console}/Command/AbstractCommand.php (75%) rename src/{ => Console}/Command/CodeStyleCommand.php (89%) rename src/{ => Console}/Command/CopyLicenseCommand.php (66%) rename src/{ => Console}/Command/DependenciesCommand.php (76%) rename src/{ => Console}/Command/DocsCommand.php (89%) rename src/{ => Console}/Command/GitAttributesCommand.php (74%) rename src/{ => Console}/Command/GitIgnoreCommand.php (78%) rename src/{ => Console}/Command/PhpDocCommand.php (90%) rename src/{ => Console}/Command/RefactorCommand.php (90%) rename src/{ => Console}/Command/ReportsCommand.php (75%) rename src/{ => Console}/Command/SkillsCommand.php (70%) rename src/{ => Console}/Command/StandardsCommand.php (87%) rename src/{ => Console}/Command/SyncCommand.php (88%) rename src/{ => Console}/Command/TestsCommand.php (94%) rename src/{ => Console}/Command/WikiCommand.php (78%) create mode 100644 src/Console/CommandLoader/DevToolsCommandLoader.php create mode 100644 src/Console/DevTools.php delete mode 100644 src/DevTools.php create mode 100644 src/Psr/Clock/SystemClock.php create mode 100644 src/Psr/Container/Container.php diff --git a/bin/dev-tools.php b/bin/dev-tools.php index b6d1646..2b24eef 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -18,6 +18,7 @@ namespace FastForward\DevTools; +use FastForward\DevTools\Psr\Container\Container; use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; @@ -25,5 +26,5 @@ require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -$application = new DevTools(); +$application = Container::get(Console\DevTools::class); $application->run(new ArgvInput([...$argv, '--no-plugins'])); diff --git a/composer.json b/composer.json index b18f188..a776df3 100644 --- a/composer.json +++ b/composer.json @@ -36,11 +36,13 @@ "icanhazstring/composer-unused": "^0.9.6", "jolicode/jolinotif": "^3.3", "nikic/php-parser": "^5.7", + "php-di/php-di": "^7.1", "phpdocumentor/shim": "^3.9", "phpro/grumphp": "^2.19", "phpspec/prophecy": "^1.26", "phpspec/prophecy-phpunit": "^2.5", "phpunit/phpunit": "^12.5", + "psr/clock": "^1.0", "psr/log": "^3.0", "pyrech/composer-changelogs": "^2.2", "rector/rector": "^2.3", diff --git a/config/container.php b/config/container.php new file mode 100644 index 0000000..ddd75f1 --- /dev/null +++ b/config/container.php @@ -0,0 +1,51 @@ + create(Finder::class), + + // PSR + LoggerInterface::class => get(NullLogger::class), + ClockInterface::class => get(Psr\Clock\SystemClock::class), + + // Console + CommandLoaderInterface::class => get(DevToolsCommandLoader::class), + CommandProvider::class => get(Composer\Capability\DevToolsCommandProvider::class), + + // Coverage + PhpUnit\Coverage\CoverageSummaryLoaderInterface::class => get(PhpUnit\Coverage\CoverageSummaryLoader::class), + + // GitIgnore + GitIgnore\MergerInterface::class => get(GitIgnore\Merger::class), + GitIgnore\ReaderInterface::class => get(GitIgnore\Reader::class), + GitIgnore\WriterInterface::class => get(GitIgnore\Writer::class), + + // GitAttributes + GitAttributes\CandidateProviderInterface::class => get(GitAttributes\CandidateProvider::class), + GitAttributes\ExistenceCheckerInterface::class => get(GitAttributes\ExistenceChecker::class), + GitAttributes\ExportIgnoreFilterInterface::class => get(GitAttributes\ExportIgnoreFilter::class), + GitAttributes\MergerInterface::class => get(GitAttributes\Merger::class), + GitAttributes\ReaderInterface::class => get(GitAttributes\Reader::class), + GitAttributes\WriterInterface::class => get(GitAttributes\Writer::class), + + // License + License\GeneratorInterface::class => get(License\Generator::class), + License\PlaceholderResolverInterface::class => get(License\PlaceholderResolver::class), + License\ReaderInterface::class => get(License\Reader::class), + License\ResolverInterface::class => get(License\Resolver::class), + License\TemplateLoaderInterface::class => get(License\TemplateLoader::class), +]; diff --git a/docs/advanced/consumer-automation.rst b/docs/advanced/consumer-automation.rst index abb7c66..a8d680d 100644 --- a/docs/advanced/consumer-automation.rst +++ b/docs/advanced/consumer-automation.rst @@ -52,5 +52,5 @@ Producer Impact Any change to ``resources/github-actions``, ``resources/dependabot.yml``, ``.agents/skills``, ``.github/workflows``, or -``FastForward\DevTools\Command\SyncCommand`` changes the default onboarding +``FastForward\DevTools\Console\Command\SyncCommand`` changes the default onboarding story for every consumer library. diff --git a/docs/api/commands.rst b/docs/api/commands.rst index 07628a7..736ffc2 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -2,7 +2,7 @@ Command Classes =============== All public CLI commands extend -``FastForward\DevTools\Command\AbstractCommand``, which provides path +``FastForward\DevTools\Console\Command\AbstractCommand``, which provides path resolution, configuration fallback, PSR-4 lookup, and child-command dispatch. .. list-table:: @@ -11,47 +11,47 @@ resolution, configuration fallback, PSR-4 lookup, and child-command dispatch. * - Class - CLI command - Responsibility - * - ``FastForward\DevTools\Command\AbstractCommand`` + * - ``FastForward\DevTools\Console\Command\AbstractCommand`` - n/a - Shared helpers for path resolution, packaged fallback files, PSR-4 discovery, and subcommand execution. - * - ``FastForward\DevTools\Command\StandardsCommand`` + * - ``FastForward\DevTools\Console\Command\StandardsCommand`` - ``standards`` - Runs the full quality pipeline. - * - ``FastForward\DevTools\Command\RefactorCommand`` + * - ``FastForward\DevTools\Console\Command\RefactorCommand`` - ``refactor`` - Runs Rector with local or packaged configuration. - * - ``FastForward\DevTools\Command\PhpDocCommand`` + * - ``FastForward\DevTools\Console\Command\PhpDocCommand`` - ``phpdoc`` - Runs PHP-CS-Fixer and a focused Rector PHPDoc pass. - * - ``FastForward\DevTools\Command\CodeStyleCommand`` + * - ``FastForward\DevTools\Console\Command\CodeStyleCommand`` - ``code-style`` - Runs Composer Normalize and ECS. - * - ``FastForward\DevTools\Command\TestsCommand`` + * - ``FastForward\DevTools\Console\Command\TestsCommand`` - ``tests`` - Runs PHPUnit with optional coverage output. - * - ``FastForward\DevTools\Command\DependenciesCommand`` + * - ``FastForward\DevTools\Console\Command\DependenciesCommand`` - ``dependencies`` - Reports missing and unused Composer dependencies. - * - ``FastForward\DevTools\Command\DocsCommand`` + * - ``FastForward\DevTools\Console\Command\DocsCommand`` - ``docs`` - Builds the HTML documentation site. - * - ``FastForward\DevTools\Command\WikiCommand`` + * - ``FastForward\DevTools\Console\Command\WikiCommand`` - ``wiki`` - Builds Markdown API documentation. - * - ``FastForward\DevTools\Command\ReportsCommand`` + * - ``FastForward\DevTools\Console\Command\ReportsCommand`` - ``reports`` - Combines the documentation build with coverage generation. - * - ``FastForward\DevTools\Command\SkillsCommand`` + * - ``FastForward\DevTools\Console\Command\SkillsCommand`` - ``skills`` - Synchronizes packaged agent skills into ``.agents/skills``. - * - ``FastForward\DevTools\Command\SyncCommand`` + * - ``FastForward\DevTools\Console\Command\SyncCommand`` - ``dev-tools:sync`` - Synchronizes consumer-facing scripts, automation assets, and packaged skills. - * - ``FastForward\DevTools\Command\GitIgnoreCommand`` + * - ``FastForward\DevTools\Console\Command\GitIgnoreCommand`` - ``gitignore`` - Merges and synchronizes .gitignore files. - * - ``FastForward\DevTools\Command\CopyLicenseCommand`` + * - ``FastForward\DevTools\Console\Command\CopyLicenseCommand`` - ``license`` - Generates a LICENSE file from composer.json license information. diff --git a/docs/configuration/overriding-defaults.rst b/docs/configuration/overriding-defaults.rst index f1f6b18..746634f 100644 --- a/docs/configuration/overriding-defaults.rst +++ b/docs/configuration/overriding-defaults.rst @@ -7,7 +7,7 @@ without forking the whole package. Resolution Order ---------------- -``FastForward\DevTools\Command\AbstractCommand::getConfigFile()`` resolves +``FastForward\DevTools\Console\Command\AbstractCommand::getConfigFile()`` resolves configuration in this order: 1. Check whether the file exists in the current working directory. diff --git a/docs/faq.rst b/docs/faq.rst index daf3c57..bfa9256 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -33,13 +33,13 @@ after deleting a packaged skill link locally. Why does ``code-style`` touch ``composer.lock``? ------------------------------------------------ -Because ``FastForward\DevTools\Command\CodeStyleCommand`` always runs +Because ``FastForward\DevTools\Console\Command\CodeStyleCommand`` always runs ``composer update --lock --quiet`` before Composer Normalize and ECS. Where did ``.docheader`` come from? ----------------------------------- -``FastForward\DevTools\Command\PhpDocCommand`` creates it automatically when it +``FastForward\DevTools\Console\Command\PhpDocCommand`` creates it automatically when it is missing. The template comes from the packaged file and is rewritten with the current package name when possible. diff --git a/docs/internals/architecture.rst b/docs/internals/architecture.rst index 6595199..7a8abb9 100644 --- a/docs/internals/architecture.rst +++ b/docs/internals/architecture.rst @@ -16,7 +16,7 @@ Local Command Lifecycle is given. 5. Individual commands resolve local configuration first and packaged fallbacks second through - ``FastForward\DevTools\Command\AbstractCommand::getConfigFile()``. + ``FastForward\DevTools\Console\Command\AbstractCommand::getConfigFile()``. Consumer Synchronization Lifecycle ---------------------------------- @@ -27,10 +27,10 @@ Consumer Synchronization Lifecycle ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider``. 4. After ``composer install`` or ``composer update``, the plugin runs ``vendor/bin/dev-tools dev-tools:sync``. -5. ``FastForward\DevTools\Command\SyncCommand`` updates scripts, GitHub +5. ``FastForward\DevTools\Console\Command\SyncCommand`` updates scripts, GitHub workflow stubs, ``.editorconfig``, ``dependabot.yml``, ``.gitignore``, and the wiki submodule in the consumer repository. -6. ``FastForward\DevTools\Command\SkillsCommand`` synchronizes packaged skill +6. ``FastForward\DevTools\Console\Command\SkillsCommand`` synchronizes packaged skill links into the consumer ``.agents/skills`` directory. 7. ``FastForward\DevTools\Agent\Skills\SkillsSynchronizer`` creates missing links, repairs broken ones, and preserves consumer-owned directories. @@ -38,20 +38,20 @@ Consumer Synchronization Lifecycle Documentation Pipeline ---------------------- -1. ``FastForward\DevTools\Command\DocsCommand`` reads PSR-4 paths from +1. ``FastForward\DevTools\Console\Command\DocsCommand`` reads PSR-4 paths from ``composer.json``. 2. It generates a temporary ``phpdocumentor.xml`` file in ``tmp/cache/phpdoc``. 3. phpDocumentor builds API pages from those PSR-4 paths. 4. phpDocumentor also builds the guide from the selected ``docs/`` source directory. -5. ``FastForward\DevTools\Command\ReportsCommand`` combines that +5. ``FastForward\DevTools\Console\Command\ReportsCommand`` combines that documentation build with PHPUnit coverage generation. Key Abstraction --------------- -``FastForward\DevTools\Command\AbstractCommand`` is the main shared layer. It +``FastForward\DevTools\Console\Command\AbstractCommand`` is the main shared layer. It centralizes: - current working directory detection; diff --git a/src/Agent/Skills/SkillsSynchronizer.php b/src/Agent/Skills/SkillsSynchronizer.php index 9386ce8..9486d9d 100644 --- a/src/Agent/Skills/SkillsSynchronizer.php +++ b/src/Agent/Skills/SkillsSynchronizer.php @@ -20,6 +20,7 @@ use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -34,21 +35,24 @@ */ final class SkillsSynchronizer implements LoggerAwareInterface { - use LoggerAwareTrait; - /** - * Initializes the synchronizer with an optional filesystem instance. - * - * If no filesystem is provided, a default {@see Filesystem} instance is created. + * Initializes the synchronizer with a filesystem and finder instance. * - * @param Filesystem|null $filesystem Filesystem instance for file operations + * @param Filesystem $filesystem Filesystem instance for file operations * @param Finder $finder Finder instance for locating skill directories in the package */ public function __construct( - private readonly Filesystem $filesystem = new Filesystem(), - private readonly Finder $finder = new Finder(), - ) { - $this->logger = new NullLogger(); + private readonly Filesystem $filesystem, + private readonly Finder $finder, + private LoggerInterface $logger, + ) {} + + /** + * {@inheritDoc} + */ + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; } /** diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index f356b50..f847183 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -18,54 +18,21 @@ namespace FastForward\DevTools\Composer\Capability; -use FastForward\DevTools\Command\AbstractCommand; -use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability; -use FastForward\DevTools\Command\CodeStyleCommand; -use FastForward\DevTools\Command\CopyLicenseCommand; -use FastForward\DevTools\Command\DependenciesCommand; -use FastForward\DevTools\Command\DocsCommand; -use FastForward\DevTools\Command\GitAttributesCommand; -use FastForward\DevTools\Command\GitIgnoreCommand; -use FastForward\DevTools\Command\PhpDocCommand; -use FastForward\DevTools\Command\RefactorCommand; -use FastForward\DevTools\Command\ReportsCommand; -use FastForward\DevTools\Command\StandardsCommand; -use FastForward\DevTools\Command\TestsCommand; -use FastForward\DevTools\Command\WikiCommand; -use FastForward\DevTools\Command\SyncCommand; -use FastForward\DevTools\Command\SkillsCommand; +use Composer\Plugin\Capability\CommandProvider; +use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Psr\Container\Container; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. - * This capability struct MUST implement the defined `CommandProviderCapability`. + * This capability struct MUST implement the defined `CommandProvider`. */ -final class DevToolsCommandProvider implements CommandProviderCapability +final class DevToolsCommandProvider implements CommandProvider { /** - * Dispatches the comprehensive collection of CLI commands. - * - * The method MUST yield an array of instantiated command classes representing the tools. - * It SHALL be queried by the Composer plugin dynamically during runtime execution. - * - * @return array the commands defined within the toolset + * {@inheritDoc} */ public function getCommands() { - return [ - new CodeStyleCommand(), - new RefactorCommand(), - new TestsCommand(), - new DependenciesCommand(), - new PhpDocCommand(), - new DocsCommand(), - new StandardsCommand(), - new ReportsCommand(), - new WikiCommand(), - new SyncCommand(), - new GitIgnoreCommand(), - new GitAttributesCommand(), - new SkillsCommand(), - new CopyLicenseCommand(), - ]; + return Container::get(DevTools::class)->getCommands(); } } diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php new file mode 100644 index 0000000..fe55b5b --- /dev/null +++ b/src/Composer/Json/ComposerJson.php @@ -0,0 +1,59 @@ +data = $this->read(); + } + + public function getPackageName(): string + { + return $this->data['name'] ?? ''; + } + + public function getPackageDescription(): string + { + return $this->data['description'] ?? ''; + } + + public function getPackageLicense(): ?string + { + $license = $this->data['license'] ?? []; + + if (\is_string($license)) { + return $license; + } + + if (\is_array($license) && count($license) === 1) { + return $license[0]; + } + + return null; + } + + public function getAuthors(): array + { + return $this->data['authors'] ?? []; + } + + public function getExtra(): array + { + return $this->data['extra'] ?? []; + } + + public function getAutoload(string $type = 'psr-4'): array + { + $autoload = $this->data['autoload'] ?? []; + + return $autoload[$type] ?? []; + } +} diff --git a/src/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php similarity index 75% rename from src/Command/AbstractCommand.php rename to src/Console/Command/AbstractCommand.php index 643d5d5..b82796b 100644 --- a/src/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -16,7 +16,7 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; use RuntimeException; use Symfony\Component\Console\Helper\ProcessHelper; @@ -36,22 +36,15 @@ */ abstract class AbstractCommand extends BaseCommand { - /** - * @var Filesystem The filesystem instance used for file operations. This property MUST be utilized for interacting with the file system securely. - */ - protected readonly Filesystem $filesystem; /** * Constructs a new AbstractCommand instance. * - * The method MAY accept a Filesystem instance; if omitted, it SHALL instantiate a new one. - * - * @param Filesystem|null $filesystem the filesystem utility to use + * @param Filesystem $filesystem the filesystem utility to use */ - public function __construct(?Filesystem $filesystem = null) - { - $this->filesystem = $filesystem ?? new Filesystem(); - + public function __construct( + protected readonly Filesystem $filesystem + ) { parent::__construct(); } @@ -186,53 +179,4 @@ protected function runCommand(string $command, OutputInterface $output): int return $this->getApplication() ->doRun(new StringInput($command), $output); } - - /** - * Retrieves configured PSR-4 namespaces from the composer configuration. - * - * This method SHALL parse the underlying `composer.json` using the Composer instance, - * and MUST provide an empty array if no specific paths exist. - * - * @return array the PSR-4 namespaces mappings - */ - protected function getPsr4Namespaces(): array - { - $composer = $this->requireComposer(); - $autoload = $composer->getPackage() - ->getAutoload(); - - return $autoload['psr-4'] ?? []; - } - - /** - * Computes the human-readable title or description of the current application. - * - * The method SHOULD utilize the package description as the title, but MUST provide - * the raw package name as a fallback mechanism. - * - * @return string the computed title or description string - */ - protected function getProjectName(): string - { - $composer = $this->requireComposer(); - $package = $composer->getPackage(); - - return $package->getName(); - } - - /** - * Computes the human-readable description of the current application. - * - * The method SHOULD utilize the package description as the title, but MUST provide - * the raw package name as a fallback mechanism. - * - * @return string the computed title or description string - */ - protected function getProjectDescription(): string - { - $composer = $this->requireComposer(); - $package = $composer->getPackage(); - - return $package->getDescription(); - } } diff --git a/src/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php similarity index 89% rename from src/Command/CodeStyleCommand.php rename to src/Console/Command/CodeStyleCommand.php index bf98d82..4d229cb 100644 --- a/src/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -16,8 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -27,6 +28,11 @@ * Represents the command responsible for checking and fixing code style issues. * 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.', + help: 'This command runs EasyCodingStandard and Composer Normalize to check and fix code style issues.' +)] final class CodeStyleCommand extends AbstractCommand { /** @@ -45,9 +51,6 @@ final class CodeStyleCommand extends AbstractCommand protected function configure(): void { $this - ->setName('code-style') - ->setDescription('Checks and fixes code style issues using EasyCodingStandard and Composer Normalize.') - ->setHelp('This command runs EasyCodingStandard and Composer Normalize to check and fix code style issues.') ->addOption( name: 'fix', shortcut: 'f', diff --git a/src/Command/CopyLicenseCommand.php b/src/Console/Command/CopyLicenseCommand.php similarity index 66% rename from src/Command/CopyLicenseCommand.php rename to src/Console/Command/CopyLicenseCommand.php index b09f6e4..ae0f7e5 100644 --- a/src/Command/CopyLicenseCommand.php +++ b/src/Console/Command/CopyLicenseCommand.php @@ -16,7 +16,7 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; use Composer\Factory; use FastForward\DevTools\License\Generator; @@ -26,6 +26,7 @@ use FastForward\DevTools\License\Resolver; use FastForward\DevTools\License\TemplateLoader; use SplFileObject; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; @@ -36,50 +37,26 @@ * 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.', + help: 'This command generates a LICENSE file if one does not exist and a supported license is declared in composer.json.' +)] final class CopyLicenseCommand extends AbstractCommand { /** * Creates a new CopyLicenseCommand instance. * - * @param Filesystem|null $filesystem the filesystem component - * @param GeneratorInterface|null $generator the generator component + * @param GeneratorInterface $generator the generator component + * @param Filesystem $filesystem the filesystem component */ public function __construct( - ?Filesystem $filesystem = null, - private readonly ?GeneratorInterface $generator = null, + private readonly GeneratorInterface $generator, + Filesystem $filesystem, ) { parent::__construct($filesystem); } - /** - * @return GeneratorInterface - */ - private function getGenerator(): GeneratorInterface - { - return $this->generator ?? new Generator( - new Reader(new SplFileObject(Factory::getComposerFile())), - new Resolver(), - new TemplateLoader(), - new PlaceholderResolver(), - $this->filesystem, - ); - } - - /** - * Configures the current command. - * - * This method MUST define the name, description, and help text for the command. - */ - protected function configure(): void - { - $this - ->setName('license') - ->setDescription('Generates a LICENSE file from composer.json license information.') - ->setHelp( - 'This command generates a LICENSE file if one does not exist and a supported license is declared in composer.json.' - ); - } - /** * Executes the license generation process. * @@ -100,8 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } - $license = $this->getGenerator() - ->generate($targetPath); + $license = $this->generator->generate($targetPath); if (null === $license) { $output->writeln( diff --git a/src/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php similarity index 76% rename from src/Command/DependenciesCommand.php rename to src/Console/Command/DependenciesCommand.php index f7f533b..5ca0762 100644 --- a/src/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -16,8 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; @@ -27,28 +28,14 @@ * This command MUST report missing and unused dependencies using a single, * deterministic report that is friendly for local development and CI runs. */ +#[AsCommand( + name: 'dependencies', + description: 'Analyzes missing and unused Composer dependencies.', + help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.', + aliases: ['deps'] +)] final class DependenciesCommand extends AbstractCommand { - /** - * Configures the dependency analysis command metadata. - * - * The command MUST expose the `dependencies` name so it can run via both - * Composer and the local `dev-tools` binary. - * - * @return void - */ - protected function configure(): void - { - $this - ->setName('dependencies') - ->setAliases(['deps']) - ->setDescription('Analyzes missing and unused Composer dependencies.') - ->setHelp( - 'This command runs composer-dependency-analyser and composer-unused to report ' - . 'missing and unused Composer dependencies.' - ); - } - /** * Executes the dependency analysis workflow. * diff --git a/src/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php similarity index 89% rename from src/Command/DocsCommand.php rename to src/Console/Command/DocsCommand.php index 6d8be36..83653ba 100644 --- a/src/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -16,11 +16,14 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use FastForward\DevTools\Composer\Json\ComposerJson; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Path; use Symfony\Component\Process\Process; @@ -34,8 +37,20 @@ * 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: 'docs', + description: 'Generates API documentation.', + help: 'This command generates API documentation using phpDocumentor.', +)] final class DocsCommand extends AbstractCommand { + public function __construct( + private readonly ComposerJson $composerJson, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the command instance. * @@ -47,9 +62,6 @@ final class DocsCommand extends AbstractCommand protected function configure(): void { $this - ->setName('docs') - ->setDescription('Generates API documentation.') - ->setHelp('This command generates API documentation using phpDocumentor.') ->addOption( name: 'target', shortcut: 't', @@ -133,7 +145,7 @@ private function createPhpDocumentorConfig(string $source, string $target, strin $this->filesystem->mkdir($configDirectory); } - $psr4Namespaces = $this->getPsr4Namespaces(); + $psr4Namespaces = $this->composerJson->getAutoload(); $paths = implode("\n", array_map( fn(string $path): string => \sprintf( '%s', @@ -148,7 +160,7 @@ private function createPhpDocumentorConfig(string $source, string $target, strin $templateContents = file_get_contents($templateFile); $this->filesystem->dumpFile($configFile, strtr($templateContents, [ - '%%TITLE%%' => $this->getProjectDescription(), + '%%TITLE%%' => $this->composerJson->getPackageDescription(), '%%TEMPLATE%%' => $template, '%%TARGET%%' => $target, '%%WORKING_DIRECTORY%%' => $workingDirectory, diff --git a/src/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php similarity index 74% rename from src/Command/GitAttributesCommand.php rename to src/Console/Command/GitAttributesCommand.php index 5ed12fc..d2c7b6d 100644 --- a/src/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -16,20 +16,16 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; -use FastForward\DevTools\GitAttributes\CandidateProvider; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\GitAttributes\CandidateProviderInterface; -use FastForward\DevTools\GitAttributes\ExistenceChecker; use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; -use FastForward\DevTools\GitAttributes\Merger; use FastForward\DevTools\GitAttributes\MergerInterface; -use FastForward\DevTools\GitAttributes\Reader; use FastForward\DevTools\GitAttributes\ReaderInterface; -use FastForward\DevTools\GitAttributes\Writer; use FastForward\DevTools\GitAttributes\WriterInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; @@ -41,6 +37,12 @@ * This command adds export-ignore entries for repository-only files and directories * to keep them out of Composer package archives. */ +#[AsCommand( + name: 'gitattributes', + description: 'Manages .gitattributes export-ignore rules for leaner package archives.', + help: 'This command adds export-ignore entries for repository-only files and directories to keep them out of Composer package archives. ' + . 'Only paths that exist in the repository are added, existing custom rules are preserved, and "extra.gitattributes.keep-in-export" paths stay in exported archives.' +)] final class GitAttributesCommand extends AbstractCommand { private const string EXTRA_NAMESPACE = 'gitattributes'; @@ -49,50 +51,28 @@ final class GitAttributesCommand extends AbstractCommand private const string EXTRA_NO_EXPORT_IGNORE = 'no-export-ignore'; - private readonly WriterInterface $writer; - /** * Creates a new GitAttributesCommand instance. * - * @param Filesystem|null $filesystem the filesystem component * @param CandidateProviderInterface $candidateProvider the candidate provider * @param ExistenceCheckerInterface $existenceChecker the repository path existence checker * @param ExportIgnoreFilterInterface $exportIgnoreFilter the configured candidate filter * @param MergerInterface $merger the merger component * @param ReaderInterface $reader the reader component - * @param WriterInterface|null $writer the writer component + * @param WriterInterface $writer the writer component + * @param Filesystem $filesystem the filesystem component */ public function __construct( - ?Filesystem $filesystem = null, - private readonly CandidateProviderInterface $candidateProvider = new CandidateProvider(), - private readonly ExistenceCheckerInterface $existenceChecker = new ExistenceChecker(), - private readonly ExportIgnoreFilterInterface $exportIgnoreFilter = new ExportIgnoreFilter(), - private readonly MergerInterface $merger = new Merger(), - private readonly ReaderInterface $reader = new Reader(), - ?WriterInterface $writer = null, + private readonly CandidateProviderInterface $candidateProvider, + private readonly ExistenceCheckerInterface $existenceChecker, + private readonly ExportIgnoreFilterInterface $exportIgnoreFilter, + private readonly MergerInterface $merger, + private readonly ReaderInterface $reader, + private readonly WriterInterface $writer, + private readonly ComposerJson $composerJson, + Filesystem $filesystem, ) { parent::__construct($filesystem); - $this->writer = $writer ?? new Writer($this->filesystem); - } - - /** - * Configures the current command. - * - * This method MUST define the name, description, and help text for the command. - * - * @return void - */ - protected function configure(): void - { - $this - ->setName('gitattributes') - ->setDescription('Manages .gitattributes export-ignore rules for leaner package archives.') - ->setHelp( - 'This command adds export-ignore entries for repository-only files and directories ' - . 'to keep them out of Composer package archives. Only paths that exist in the ' - . 'repository are added, existing custom rules are preserved, and ' - . '"extra.gitattributes.keep-in-export" paths stay in exported archives.' - ); } /** @@ -150,9 +130,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function configuredKeepInExportPaths(): array { - $extra = $this->requireComposer() - ->getPackage() - ->getExtra(); + $extra = $this->composerJson->getExtra(); $gitattributesConfig = $extra[self::EXTRA_NAMESPACE] ?? null; diff --git a/src/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php similarity index 78% rename from src/Command/GitIgnoreCommand.php rename to src/Console/Command/GitIgnoreCommand.php index 0640775..f8eec96 100644 --- a/src/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -16,14 +16,12 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; -use FastForward\DevTools\GitIgnore\Merger; use FastForward\DevTools\GitIgnore\MergerInterface; -use FastForward\DevTools\GitIgnore\Reader; use FastForward\DevTools\GitIgnore\ReaderInterface; -use FastForward\DevTools\GitIgnore\Writer; use FastForward\DevTools\GitIgnore\WriterInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -38,29 +36,28 @@ * 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.', + help: 'This command merges the canonical .gitignore from dev-tools with the project\'s existing .gitignore.' +)] final class GitIgnoreCommand extends AbstractCommand { - /** - * @param WriterInterface $writer the writer component for handling .gitignore file writing - */ - private readonly WriterInterface $writer; - /** * Creates a new GitIgnoreCommand instance. * - * @param Filesystem|null $filesystem the filesystem component * @param MergerInterface $merger the merger component * @param ReaderInterface $reader the reader component * @param WriterInterface|null $writer the writer component + * @param Filesystem $filesystem the filesystem component */ public function __construct( - ?Filesystem $filesystem = null, - private readonly MergerInterface $merger = new Merger(), - private readonly ReaderInterface $reader = new Reader(), - ?WriterInterface $writer = null + private readonly MergerInterface $merger, + private readonly ReaderInterface $reader, + private readonly WriterInterface $writer, + Filesystem $filesystem, ) { parent::__construct($filesystem); - $this->writer = $writer ?? new Writer($this->filesystem); } /** @@ -72,11 +69,6 @@ public function __construct( protected function configure(): void { $this - ->setName('gitignore') - ->setDescription('Merges and synchronizes .gitignore files.') - ->setHelp( - "This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore." - ) ->addOption( name: 'source', shortcut: 's', diff --git a/src/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php similarity index 90% rename from src/Command/PhpDocCommand.php rename to src/Console/Command/PhpDocCommand.php index de5a65e..111fdd9 100644 --- a/src/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -16,13 +16,16 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use FastForward\DevTools\Composer\Json\ComposerJson; use Throwable; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; use function Safe\file_get_contents; @@ -31,6 +34,11 @@ * 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.', + help: 'This command checks and fixes PHPDocs in your PHP files.', +)] final class PhpDocCommand extends AbstractCommand { /** @@ -43,6 +51,13 @@ final class PhpDocCommand extends AbstractCommand */ public const string CONFIG = '.php-cs-fixer.dist.php'; + public function __construct( + private readonly ComposerJson $composerJson, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the PHPDoc command. * @@ -53,9 +68,6 @@ final class PhpDocCommand extends AbstractCommand protected function configure(): void { $this - ->setName('phpdoc') - ->setDescription('Checks and fixes PHPDocs.') - ->setHelp('This command checks and fixes PHPDocs in your PHP files.') ->addOption( name: 'fix', shortcut: 'f', @@ -170,17 +182,7 @@ private function ensureDocHeaderExists(OutputInterface $output): void $repositoryDocHeader = self::getConfigFile(self::FILENAME); $docHeader = file_get_contents($repositoryDocHeader); - - try { - $composer = $this->requireComposer(); - $rootPackageName = $composer->getPackage() - ->getName(); - - if ('' !== $rootPackageName) { - $docHeader = str_replace('fast-forward/dev-tools', $rootPackageName, $docHeader); - } - } catch (Throwable) { - } + $docHeader = str_replace('fast-forward/dev-tools', $this->composerJson->getPackageName(), $docHeader); try { $this->filesystem->dumpFile($projectDocHeader, $docHeader); diff --git a/src/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php similarity index 90% rename from src/Command/RefactorCommand.php rename to src/Console/Command/RefactorCommand.php index b6a8655..e2768d5 100644 --- a/src/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -16,8 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -27,6 +28,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.', + help: 'This command runs Rector to refactor your code.', + aliases: ['rector'] +)] final class RefactorCommand extends AbstractCommand { /** @@ -45,9 +52,6 @@ final class RefactorCommand extends AbstractCommand protected function configure(): void { $this - ->setName('refactor') - ->setDescription('Runs Rector for code refactoring.') - ->setHelp('This command runs Rector to refactor your code.') ->addOption( name: 'fix', shortcut: 'f', diff --git a/src/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php similarity index 75% rename from src/Command/ReportsCommand.php rename to src/Console/Command/ReportsCommand.php index 4508e06..e44f400 100644 --- a/src/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -16,8 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -25,25 +26,13 @@ * 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.', + help: 'This command generates the frontpage for Fast Forward documentation, including links to API documentation and test reports.' +)] final class ReportsCommand extends AbstractCommand { - /** - * Configures the metadata for the reports generation command. - * - * The method MUST identify the command correctly and describe its intent broadly. - * - * @return void - */ - protected function configure(): void - { - $this - ->setName('reports') - ->setDescription('Generates the frontpage for Fast Forward documentation.') - ->setHelp( - 'This command generates the frontpage for Fast Forward documentation, including links to API documentation and test reports.' - ); - } - /** * Executes the generation logic for diverse reports. * @@ -70,8 +59,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln('Generating test coverage report on path: ' . $coveragePath . ''); $results[] = $this->runCommand('tests --coverage=' . $coveragePath, $output); - $output->writeln('Frontpage generation completed!'); - return \in_array(self::FAILURE, $results, true) ? self::FAILURE : self::SUCCESS; } } diff --git a/src/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php similarity index 70% rename from src/Command/SkillsCommand.php rename to src/Console/Command/SkillsCommand.php index 169bc41..c9b6923 100644 --- a/src/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -16,9 +16,10 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Agent\Skills\SkillsSynchronizer; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; @@ -37,50 +38,30 @@ * 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.', + help: 'This command ensures the consumer repository contains linked Fast Forward skills by creating symlinks to the packaged skills and removing broken links.' +)] final class SkillsCommand extends AbstractCommand { /** * Initializes the command with an optional skills synchronizer instance. * - * If no synchronizer is provided, the command SHALL instantiate the default - * {@see SkillsSynchronizer} implementation. Consumers MAY inject a custom - * synchronizer for testing or alternative synchronization behavior, provided - * it preserves the expected contract. - * - * @param SkillsSynchronizer|null $synchronizer the synchronizer responsible - * for applying the skills - * synchronization process - * @param Filesystem|null $filesystem filesystem used to resolve - * and manage the skills - * directory structure + * @param SkillsSynchronizer $synchronizer the synchronizer responsible + * for applying the skills + * synchronization process + * @param Filesystem $filesystem filesystem used to resolve + * and manage the skills + * directory structure */ public function __construct( - private readonly SkillsSynchronizer $synchronizer = new SkillsSynchronizer(), - ?Filesystem $filesystem = null + private readonly SkillsSynchronizer $synchronizer, + Filesystem $filesystem, ) { parent::__construct($filesystem); } - /** - * Configures the command name, description, and help text. - * - * The command metadata MUST clearly describe that the operation synchronizes - * Fast Forward skills into the `.agents/skills` directory and that it manages - * link-based synchronization for packaged skills. - * - * @return void - */ - protected function configure(): void - { - $this - ->setName('skills') - ->setDescription('Synchronizes Fast Forward skills into .agents/skills directory.') - ->setHelp( - 'This command ensures the consumer repository contains linked Fast Forward skills ' - . 'by creating symlinks to the packaged skills and removing broken links.' - ); - } - /** * Executes the skills synchronization workflow. * diff --git a/src/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php similarity index 87% rename from src/Command/StandardsCommand.php rename to src/Console/Command/StandardsCommand.php index 11cbb88..c2b556a 100644 --- a/src/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -16,8 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -26,6 +27,11 @@ * 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.', + help: 'This command runs all Fast Forward code standards checks, including code refactoring, PHPDoc validation, code style checks, documentation generation, and tests execution.' +)] final class StandardsCommand extends AbstractCommand { /** @@ -39,12 +45,6 @@ final class StandardsCommand extends AbstractCommand protected function configure(): void { $this - ->setName('standards') - ->setDescription('Runs Fast Forward code standards checks.') - ->setHelp( - 'This command runs all Fast Forward code standards checks, including code refactoring, ' - . 'PHPDoc validation, code style checks, documentation generation, and tests execution.' - ) ->addOption( name: 'fix', shortcut: 'f', diff --git a/src/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php similarity index 88% rename from src/Command/SyncCommand.php rename to src/Console/Command/SyncCommand.php index d2f39cb..b80de23 100644 --- a/src/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -16,10 +16,11 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; use Composer\Factory; use Composer\Json\JsonManipulator; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Path; @@ -30,30 +31,15 @@ /** * Represents the command responsible for installing development scripts into `composer.json`. - * This class MUST NOT be overridden and SHALL rely on the `ScriptsInstallerTrait`. */ +#[AsCommand( + name: 'dev-tools:sync', + description: 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, .editorconfig, and .gitattributes in the root project.', + help: 'This command adds or updates dev-tools scripts in composer.json, copies reusable GitHub Actions workflows, ensures .editorconfig is present and up to date, ' + . 'and manages .gitattributes export-ignore rules.' +)] final class SyncCommand extends AbstractCommand { - /** - * Configures the current command. - * - * This method MUST define the name, description, and help text for the command. - * It SHALL identify the tool as the mechanism for script synchronization. - * - * @return void - */ - protected function configure(): void - { - $this - ->setName('dev-tools:sync') - ->setDescription( - 'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, .editorconfig, and .gitattributes in the root project.' - ) - ->setHelp( - 'This command adds or updates dev-tools scripts in composer.json, copies reusable GitHub Actions workflows, ensures .editorconfig is present and up to date, and manages .gitattributes export-ignore rules.' - ); - } - /** * Executes the script installation block. * diff --git a/src/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php similarity index 94% rename from src/Command/TestsCommand.php rename to src/Console/Command/TestsCommand.php index fc1706c..f50516b 100644 --- a/src/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -16,12 +16,14 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; use InvalidArgumentException; use RuntimeException; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -35,6 +37,11 @@ * 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.', + help: 'This command runs PHPUnit to execute your tests.' +)] final class TestsCommand extends AbstractCommand { /** @@ -43,12 +50,14 @@ final class TestsCommand extends AbstractCommand public const string CONFIG = 'phpunit.xml'; /** - * @param Filesystem|null $filesystem the filesystem utility used for path resolution * @param CoverageSummaryLoaderInterface $coverageSummaryLoader the loader used for `coverage-php` summaries + * @param ComposerJson $composerJson the composer.json reader for autoload information + * @param Filesystem $filesystem the filesystem utility used for path resolution */ public function __construct( - ?Filesystem $filesystem = null, - private readonly CoverageSummaryLoaderInterface $coverageSummaryLoader = new CoverageSummaryLoader(), + private readonly CoverageSummaryLoaderInterface $coverageSummaryLoader, + private readonly ComposerJson $composerJson, + Filesystem $filesystem, ) { parent::__construct($filesystem); } @@ -64,9 +73,6 @@ public function __construct( protected function configure(): void { $this - ->setName('tests') - ->setDescription('Runs PHPUnit tests.') - ->setHelp('This command runs PHPUnit to execute your tests.') ->addArgument( name: 'path', mode: InputArgument::OPTIONAL, @@ -228,7 +234,7 @@ private function configureCoverageArguments( ? $this->resolvePath($input, 'coverage') : $this->resolvePath($input, 'cache-dir'); - foreach ($this->getPsr4Namespaces() as $path) { + foreach ($this->composerJson->getAutoload() as $path) { $arguments[] = '--coverage-filter=' . $this->getAbsolutePath($path); } diff --git a/src/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php similarity index 78% rename from src/Command/WikiCommand.php rename to src/Console/Command/WikiCommand.php index 406afac..96a6269 100644 --- a/src/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -16,19 +16,35 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; +use FastForward\DevTools\Composer\Json\ComposerJson; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; /** * 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.', + help: 'This command generates API documentation in Markdown format using phpDocumentor. ' + . 'It accepts an optional `--target` option to specify the output directory for the generated documentation.' +)] final class WikiCommand extends AbstractCommand { + public function __construct( + private readonly ComposerJson $composerJson, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the command instance. * @@ -40,9 +56,6 @@ final class WikiCommand extends AbstractCommand protected function configure(): void { $this - ->setName('wiki') - ->setDescription('Generates API documentation in Markdown format.') - ->setHelp('This command generates API documentation in Markdown format using phpDocumentor.') ->addOption( name: 'target', shortcut: 't', @@ -72,10 +85,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int '--cache-folder', $this->getCurrentWorkingDirectory() . '/tmp/cache/phpdoc', '--visibility=public,protected', - '--title=' . $this->getProjectDescription(), + '--title=' . $this->composerJson->getPackageDescription(), ]; - $psr4Namespaces = $this->getPsr4Namespaces(); + $psr4Namespaces = $this->composerJson->getAutoload(); foreach ($psr4Namespaces as $path) { $arguments[] = '--directory'; diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php new file mode 100644 index 0000000..16045f0 --- /dev/null +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -0,0 +1,44 @@ +getCommandMap($finder, $container)); + } + + private function getCommandMap( + Finder $finder, + ContainerInterface $container + ): array { + $commandMap = []; + + $commandsDirectory = $finder + ->files() + ->in(__DIR__ . '/../Command') + ->name('*.php'); + + foreach ($commandsDirectory as $file) { + $class = 'FastForward\\DevTools\\Console\\Command\\' . $file->getBasename('.php'); + $reflection = new \ReflectionClass($class); + + if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(Command::class)) { + continue; + } + + $command = $container->get($class); + $commandMap[$command->getName()] = $class; + } + + return $commandMap; + } +} diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php new file mode 100644 index 0000000..f631afe --- /dev/null +++ b/src/Console/DevTools.php @@ -0,0 +1,63 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Console; + +use Composer\Console\Application as ComposerApplication; +use ReflectionMethod; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; + +/** + * 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 +{ + /** + * Initializes the DevTools global context and dependency graph. + * + * The method MUST define default configurations and MAY accept an explicit command provider. + * It SHALL instruct the runner to treat the `standards` command generically as its default endpoint. + * + * @param CommandLoaderInterface $commandLoader the command loader responsible for providing command instances + */ + public function __construct( + CommandLoaderInterface $commandLoader, + ) { + parent::__construct('Fast Forward Dev Tools'); + + $this->setDefaultCommand('standards'); + $this->setCommandLoader($commandLoader); + } + + /** + * 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 + */ + protected function getDefaultCommands(): array + { + $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); + + return $reflectionMethod->invoke($this); + } +} diff --git a/src/DevTools.php b/src/DevTools.php deleted file mode 100644 index bb2dfa1..0000000 --- a/src/DevTools.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools; - -use Override; -use Composer\Console\Application; -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use Symfony\Component\Console\Command\ListCommand; -use Symfony\Component\Console\Command\CompleteCommand; -use Symfony\Component\Console\Command\DumpCompletionCommand; -use Symfony\Component\Console\Command\HelpCommand; - -/** - * 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 Application -{ - /** - * Initializes the DevTools global context and dependency graph. - * - * The method MUST define default configurations and MAY accept an explicit command provider. - * It SHALL instruct the runner to treat the `standards` command generically as its default endpoint. - * - * @param CommandProvider|null $commandProvider provides the execution references securely, defaults dynamically - */ - public function __construct( - private readonly ?CommandProvider $commandProvider = new DevToolsCommandProvider(), - ) { - parent::__construct('Fast Forward Dev Tools'); - $this->setDefaultCommand('standards'); - } - - /** - * Aggregates default processes attached safely to the environment base lifecycle. - * - * The method MUST inject core operational constraints and external definitions seamlessly. - * It SHALL execute an overriding merge logically combining provider and utility features. - * - * @return array the collected list of functional commands configured to run - */ - #[Override] - protected function getDefaultCommands(): array - { - return array_merge($this->commandProvider->getCommands(), [ - new HelpCommand(), - new ListCommand(), - new CompleteCommand(), - new DumpCompletionCommand(), - ]); - } -} diff --git a/src/License/Generator.php b/src/License/Generator.php index 5ef8d7f..5df12c0 100644 --- a/src/License/Generator.php +++ b/src/License/Generator.php @@ -49,7 +49,7 @@ public function __construct( private ResolverInterface $resolver, private TemplateLoaderInterface $templateLoader, private PlaceholderResolverInterface $placeholderResolver, - private Filesystem $filesystem = new Filesystem() + private Filesystem $filesystem, ) {} /** diff --git a/src/License/PlaceholderResolver.php b/src/License/PlaceholderResolver.php index 0a7090e..24bf4a1 100644 --- a/src/License/PlaceholderResolver.php +++ b/src/License/PlaceholderResolver.php @@ -18,6 +18,8 @@ namespace FastForward\DevTools\License; +use Psr\Clock\ClockInterface; + use function Safe\preg_replace; /** @@ -29,6 +31,10 @@ */ final class PlaceholderResolver implements PlaceholderResolverInterface { + public function __construct( + private readonly ClockInterface $clock, + ) {} + /** * Resolves placeholders in a license template with the provided metadata. * @@ -48,8 +54,10 @@ final class PlaceholderResolver implements PlaceholderResolverInterface */ public function resolve(string $template, array $metadata): string { + $now = $this->clock->now(); + $replacements = [ - '{{ year }}' => (string) ($metadata['year'] ?? date('Y')), + '{{ year }}' => (string) ($metadata['year'] ?? $now->format('Y')), '{{ organization }}' => $metadata['organization'] ?? '', '{{ author }}' => $metadata['author'] ?? '', '{{ project }}' => $metadata['project'] ?? '', diff --git a/src/License/Reader.php b/src/License/Reader.php index 5cf3166..de6a72c 100644 --- a/src/License/Reader.php +++ b/src/License/Reader.php @@ -18,10 +18,9 @@ namespace FastForward\DevTools\License; +use FastForward\DevTools\Composer\Json\ComposerJson; +use Psr\Clock\ClockInterface; use Safe\Exceptions\JsonException; -use SplFileObject; - -use function Safe\json_decode; /** * Reads composer.json and exposes metadata for license generation. @@ -37,29 +36,15 @@ /** * Creates a new Reader instance. * - * @param SplFileObject $source The source file to read from, typically composer.json + * @param ComposerJson $source The source file to read from, typically composer.json * * @throws JsonException if the JSON content is invalid */ - public function __construct(SplFileObject $source) - { - $this->data = $this->readData($source); - } - - /** - * Reads and parses the JSON content from the source file. - * - * @param SplFileObject $source The source file to read from - * - * @return array The parsed JSON data as an associative array - * - * @throws JsonException if the JSON is invalid - */ - private function readData(SplFileObject $source): array - { - $content = $source->fread($source->getSize()); - - return json_decode($content, true); + public function __construct( + private readonly ClockInterface $clock, + private readonly ComposerJson $composerJson, + ) { + $this->data = $composerJson->read(); } /** @@ -73,13 +58,7 @@ private function readData(SplFileObject $source): array */ public function getLicense(): ?string { - $license = $this->data['license'] ?? []; - - if (\is_string($license)) { - return $license; - } - - return $this->extractLicense($license); + return $this->composerJson->getPackageLicense(); } /** @@ -89,7 +68,7 @@ public function getLicense(): ?string */ public function getPackageName(): string { - return $this->data['name'] ?? ''; + return $this->composerJson->getPackageName(); } /** @@ -102,7 +81,7 @@ public function getPackageName(): string */ public function getAuthors(): array { - $authors = $this->data['authors'] ?? []; + $authors = $this->composerJson->getAuthors(); if ([] === $authors) { return []; @@ -151,29 +130,8 @@ public function getVendor(): ?string */ public function getYear(): int { - return (int) date('Y'); - } - - /** - * Extracts a single license from an array of licenses. - * - * Returns the first license if exactly one element exists. - * Returns null if the array is empty or contains multiple licenses. - * - * @param array $license The license array to extract from - * - * @return string|null a single license string, or null if extraction is not possible - */ - private function extractLicense(array $license): ?string - { - if ([] === $license) { - return null; - } - - if (1 === \count($license)) { - return $license[0]; - } + $now = $this->clock->now(); - return null; + return (int) $now->format('Y'); } } diff --git a/src/Psr/Clock/SystemClock.php b/src/Psr/Clock/SystemClock.php new file mode 100644 index 0000000..88445dc --- /dev/null +++ b/src/Psr/Clock/SystemClock.php @@ -0,0 +1,29 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Psr\Clock; + +use Psr\Clock\ClockInterface; + +final class SystemClock implements ClockInterface +{ + public function now(): \DateTimeImmutable + { + return new \DateTimeImmutable(); + } +} diff --git a/src/Psr/Container/Container.php b/src/Psr/Container/Container.php new file mode 100644 index 0000000..da7e96e --- /dev/null +++ b/src/Psr/Container/Container.php @@ -0,0 +1,35 @@ +get($id); + } + + public static function has(string $id): bool + { + + return self::boot()->has($id); + } +} diff --git a/tests/Command/AbstractCommandTest.php b/tests/Command/AbstractCommandTest.php index 14527fc..06f6cd2 100644 --- a/tests/Command/AbstractCommandTest.php +++ b/tests/Command/AbstractCommandTest.php @@ -18,7 +18,8 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\AbstractCommand; +use FastForward\DevTools\Console\Command\AbstractCommand; +use FastForward\DevTools\Console\Command\AbstractCommand as AbstractCommandBase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Input\InputInterface; @@ -28,11 +29,11 @@ final class AbstractCommandTest extends AbstractCommandTestCase { /** - * @return string + * @return AbstractCommand */ - protected function getCommandClass(): string + protected function getCommandClass(): AbstractCommand { - return AbstractCommandStub::class; + return new AbstractCommandStub($this->filesystem->reveal()); } /** diff --git a/tests/Command/AbstractCommandTestCase.php b/tests/Command/AbstractCommandTestCase.php index 99bde72..730a2bb 100644 --- a/tests/Command/AbstractCommandTestCase.php +++ b/tests/Command/AbstractCommandTestCase.php @@ -22,7 +22,7 @@ use Composer\Package\RootPackageInterface; use Composer\Console\Application; use ReflectionMethod; -use FastForward\DevTools\Command\AbstractCommand; +use FastForward\DevTools\Console\Command\AbstractCommand; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -101,6 +101,7 @@ protected function setUp(): void $this->application->getComposer(Argument::cetera())->willReturn($this->composer->reveal()); $this->filesystem->isAbsolutePath(Argument::any())->willReturn(false); + $this->filesystem->exists(Argument::any())->willReturn(false); $this->processHelper->getName() ->willReturn('process'); diff --git a/tests/Command/CodeStyleCommandTest.php b/tests/Command/CodeStyleCommandTest.php index ec5994e..68eb5a3 100644 --- a/tests/Command/CodeStyleCommandTest.php +++ b/tests/Command/CodeStyleCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\CodeStyleCommand; +use FastForward\DevTools\Console\Command\CodeStyleCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Command/CopyLicenseCommandTest.php b/tests/Command/CopyLicenseCommandTest.php index 1cf0a41..1baf297 100644 --- a/tests/Command/CopyLicenseCommandTest.php +++ b/tests/Command/CopyLicenseCommandTest.php @@ -18,8 +18,9 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\CopyLicenseCommand; +use FastForward\DevTools\Console\Command\CopyLicenseCommand; use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\GeneratorInterface; use FastForward\DevTools\License\PlaceholderResolver; use FastForward\DevTools\License\Reader; use FastForward\DevTools\License\Resolver; @@ -29,6 +30,9 @@ use PHPUnit\Framework\Attributes\UsesClass; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; + +use function Safe\getcwd; #[CoversClass(CopyLicenseCommand::class)] #[UsesClass(Reader::class)] @@ -41,11 +45,19 @@ final class CopyLicenseCommandTest extends AbstractCommandTestCase use ProphecyTrait; /** - * @return string + * @var ObjectProphecy */ - protected function getCommandClass(): string + private ObjectProphecy $generator; + + /** + * @return CopyLicenseCommand + */ + protected function getCommandClass(): CopyLicenseCommand { - return CopyLicenseCommand::class; + return new CopyLicenseCommand( + $this->generator->reveal(), + $this->filesystem->reveal() + ); } /** @@ -72,17 +84,25 @@ protected function getCommandHelp(): string return 'This command generates a LICENSE file if one does not exist and a supported license is declared in composer.json.'; } + /** + * @return void + */ + protected function setUp(): void + { + $this->generator = $this->prophesize(GeneratorInterface::class); + + parent::setUp(); + } + /** * @return void */ #[Test] public function executeWillReturnSuccessAndWriteInfo(): void { - $this->filesystem->exists(Argument::type('string'))->willReturn(false); - $this->filesystem->dumpFile(Argument::cetera())->shouldBeCalled(); - - $this->output->writeln(Argument::type('string')) - ->shouldBeCalled(); + $targetPath = getcwd() . '/LICENSE'; + $this->filesystem->exists($targetPath)->willReturn(false); + $this->generator->generate($targetPath)->willReturn(null); self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); } @@ -93,10 +113,8 @@ public function executeWillReturnSuccessAndWriteInfo(): void #[Test] public function executeWillSkipWhenLicenseFileExists(): void { - $this->filesystem->exists(Argument::type('string'))->willReturn(true); - - $this->output->writeln(Argument::type('string')) - ->shouldBeCalled(); + $targetPath = getcwd() . '/LICENSE'; + $this->filesystem->exists($targetPath)->willReturn(true); self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); } diff --git a/tests/Command/DependenciesCommandTest.php b/tests/Command/DependenciesCommandTest.php index 6a8ba4b..05b887f 100644 --- a/tests/Command/DependenciesCommandTest.php +++ b/tests/Command/DependenciesCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\DependenciesCommand; +use FastForward\DevTools\Console\Command\DependenciesCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\Argument; diff --git a/tests/Command/DocsCommandTest.php b/tests/Command/DocsCommandTest.php index d821fdf..7a88370 100644 --- a/tests/Command/DocsCommandTest.php +++ b/tests/Command/DocsCommandTest.php @@ -18,11 +18,13 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\DocsCommand; +use FastForward\DevTools\Console\Command\DocsCommand; +use FastForward\DevTools\Composer\Json\ComposerJson; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Process\Process; use function Safe\getcwd; @@ -33,11 +35,19 @@ final class DocsCommandTest extends AbstractCommandTestCase use ProphecyTrait; /** - * @return string + * @var ObjectProphecy + */ + private ObjectProphecy $composerJson; + + /** + * @return DocsCommand */ - protected function getCommandClass(): string + protected function getCommandClass(): DocsCommand { - return DocsCommand::class; + return new DocsCommand( + $this->composerJson->reveal(), + $this->filesystem->reveal() + ); } /** @@ -64,6 +74,16 @@ protected function getCommandHelp(): string return 'This command generates API documentation using phpDocumentor.'; } + /** + * @return void + */ + protected function setUp(): void + { + $this->composerJson = $this->prophesize(ComposerJson::class); + + parent::setUp(); + } + /** * @return void */ diff --git a/tests/Command/GitIgnoreCommandTest.php b/tests/Command/GitIgnoreCommandTest.php index 89da037..7eb59aa 100644 --- a/tests/Command/GitIgnoreCommandTest.php +++ b/tests/Command/GitIgnoreCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\GitIgnoreCommand; +use FastForward\DevTools\Console\Command\GitIgnoreCommand; use FastForward\DevTools\GitIgnore\GitIgnore; use FastForward\DevTools\GitIgnore\GitIgnoreInterface; use FastForward\DevTools\GitIgnore\Merger; @@ -112,10 +112,10 @@ protected function setUp(): void protected function getCommandClass(): GitIgnoreCommand { return new GitIgnoreCommand( - $this->filesystem->reveal(), $this->merger->reveal(), $this->reader->reveal(), - $this->writer->reveal() + $this->writer->reveal(), + $this->filesystem->reveal() ); } diff --git a/tests/Command/PhpDocCommandTest.php b/tests/Command/PhpDocCommandTest.php index 8437124..5e983b7 100644 --- a/tests/Command/PhpDocCommandTest.php +++ b/tests/Command/PhpDocCommandTest.php @@ -20,8 +20,8 @@ use Prophecy\Argument; use RuntimeException; -use FastForward\DevTools\Command\PhpDocCommand; -use FastForward\DevTools\Command\RefactorCommand; +use FastForward\DevTools\Console\Command\PhpDocCommand; +use FastForward\DevTools\Console\Command\RefactorCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Command/RefactorCommandTest.php b/tests/Command/RefactorCommandTest.php index 4cc98b6..a47367f 100644 --- a/tests/Command/RefactorCommandTest.php +++ b/tests/Command/RefactorCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\RefactorCommand; +use FastForward\DevTools\Console\Command\RefactorCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Command/ReportsCommandTest.php b/tests/Command/ReportsCommandTest.php index 0383a17..72bccee 100644 --- a/tests/Command/ReportsCommandTest.php +++ b/tests/Command/ReportsCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\ReportsCommand; +use FastForward\DevTools\Console\Command\ReportsCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\Argument; diff --git a/tests/Command/SkillsCommandTest.php b/tests/Command/SkillsCommandTest.php index 7bfc29e..4a90313 100644 --- a/tests/Command/SkillsCommandTest.php +++ b/tests/Command/SkillsCommandTest.php @@ -21,7 +21,7 @@ use Composer\IO\IOInterface; use FastForward\DevTools\Agent\Skills\SkillsSynchronizer; use FastForward\DevTools\Agent\Skills\SynchronizeResult; -use FastForward\DevTools\Command\SkillsCommand; +use FastForward\DevTools\Console\Command\SkillsCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; diff --git a/tests/Command/StandardsCommandTest.php b/tests/Command/StandardsCommandTest.php index 9315268..894c476 100644 --- a/tests/Command/StandardsCommandTest.php +++ b/tests/Command/StandardsCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\StandardsCommand; +use FastForward\DevTools\Console\Command\StandardsCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\Argument; diff --git a/tests/Command/SyncCommandTest.php b/tests/Command/SyncCommandTest.php index e429f50..ef0d03c 100644 --- a/tests/Command/SyncCommandTest.php +++ b/tests/Command/SyncCommandTest.php @@ -18,8 +18,8 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\GitIgnoreCommand; -use FastForward\DevTools\Command\SyncCommand; +use FastForward\DevTools\Console\Command\GitIgnoreCommand; +use FastForward\DevTools\Console\Command\SyncCommand; use FastForward\DevTools\GitAttributes\CandidateProvider; use FastForward\DevTools\GitAttributes\ExistenceChecker; use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; diff --git a/tests/Command/TestsCommandTest.php b/tests/Command/TestsCommandTest.php index ca9a5bc..fd17c27 100644 --- a/tests/Command/TestsCommandTest.php +++ b/tests/Command/TestsCommandTest.php @@ -18,7 +18,8 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\TestsCommand; +use FastForward\DevTools\Console\Command\TestsCommand; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummary; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; use Prophecy\Argument; @@ -42,12 +43,21 @@ final class TestsCommandTest extends AbstractCommandTestCase */ private ObjectProphecy $coverageSummaryLoader; + /** + * @var ObjectProphecy + */ + private ObjectProphecy $composerJson; + /** * @return TestsCommand */ protected function getCommandClass(): TestsCommand { - return new TestsCommand($this->filesystem->reveal(), $this->coverageSummaryLoader->reveal()); + return new TestsCommand( + $this->coverageSummaryLoader->reveal(), + $this->composerJson->reveal(), + $this->filesystem->reveal() + ); } /** @@ -80,6 +90,7 @@ protected function getCommandHelp(): string protected function setUp(): void { $this->coverageSummaryLoader = $this->prophesize(CoverageSummaryLoaderInterface::class); + $this->composerJson = $this->prophesize(ComposerJson::class); parent::setUp(); diff --git a/tests/Command/WikiCommandTest.php b/tests/Command/WikiCommandTest.php index 83f96d7..409a94d 100644 --- a/tests/Command/WikiCommandTest.php +++ b/tests/Command/WikiCommandTest.php @@ -18,7 +18,7 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\WikiCommand; +use FastForward\DevTools\Console\Command\WikiCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 81a7783..6693b22 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -18,22 +18,22 @@ namespace FastForward\DevTools\Tests\Composer\Capability; -use FastForward\DevTools\Command\AbstractCommand; -use FastForward\DevTools\Command\CodeStyleCommand; -use FastForward\DevTools\Command\CopyLicenseCommand; -use FastForward\DevTools\Command\DependenciesCommand; -use FastForward\DevTools\Command\DocsCommand; -use FastForward\DevTools\Command\GitAttributesCommand; -use FastForward\DevTools\Command\GitIgnoreCommand; -use FastForward\DevTools\Command\SyncCommand; -use FastForward\DevTools\Command\SkillsCommand; +use FastForward\DevTools\Console\Command\AbstractCommand; +use FastForward\DevTools\Console\Command\CodeStyleCommand; +use FastForward\DevTools\Console\Command\CopyLicenseCommand; +use FastForward\DevTools\Console\Command\DependenciesCommand; +use FastForward\DevTools\Console\Command\DocsCommand; +use FastForward\DevTools\Console\Command\GitAttributesCommand; +use FastForward\DevTools\Console\Command\GitIgnoreCommand; +use FastForward\DevTools\Console\Command\SyncCommand; +use FastForward\DevTools\Console\Command\SkillsCommand; use FastForward\DevTools\Agent\Skills\SkillsSynchronizer; -use FastForward\DevTools\Command\PhpDocCommand; -use FastForward\DevTools\Command\RefactorCommand; -use FastForward\DevTools\Command\ReportsCommand; -use FastForward\DevTools\Command\StandardsCommand; -use FastForward\DevTools\Command\TestsCommand; -use FastForward\DevTools\Command\WikiCommand; +use FastForward\DevTools\Console\Command\PhpDocCommand; +use FastForward\DevTools\Console\Command\RefactorCommand; +use FastForward\DevTools\Console\Command\ReportsCommand; +use FastForward\DevTools\Console\Command\StandardsCommand; +use FastForward\DevTools\Console\Command\TestsCommand; +use FastForward\DevTools\Console\Command\WikiCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; use FastForward\DevTools\GitAttributes\CandidateProvider; use FastForward\DevTools\GitAttributes\ExistenceChecker; From 1625a4a5b802c2c333be4156f942a6a730635da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 14:49:08 -0300 Subject: [PATCH 02/14] Refactor tests and enhance coverage for various commands and components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplified command instantiation in CopyLicenseCommandTest and DocsCommandTest. - Improved test structure and readability in GitAttributesCommandTest and PhpDocCommandTest. - Updated ReportsCommandTest to remove unnecessary command mocks. - Enhanced TestsCommandTest and WikiCommandTest with ComposerJson mocking. - Introduced ComposerJsonTest to validate ComposerJson accessors and behavior. - Added DevToolsCommandLoaderTest to ensure only instantiable commands are registered. - Refactored DevToolsTest to utilize CommandLoaderInterface for command management. - Enhanced PlaceholderResolverTest and ReaderTest with ClockInterface for date handling. - Added JoliNotifExecutionFinishedSubscriberTest to validate notification behavior on test execution. - Introduced SystemClockTest to verify current date-time retrieval. - Added ContainerTest to validate static container behavior and configuration loading. Signed-off-by: Felipe Sayão Lobato Abreu --- bin/dev-tools.php | 3 +- config/container.php | 67 ++++++-- src/Agent/Skills/SkillsSynchronizer.php | 3 +- .../Capability/DevToolsCommandProvider.php | 6 +- src/Composer/Json/ComposerJson.php | 45 +++++- src/Console/Command/AbstractCommand.php | 3 +- src/Console/Command/CopyLicenseCommand.php | 7 - src/Console/Command/DependenciesCommand.php | 9 +- src/Console/Command/DocsCommand.php | 4 + src/Console/Command/GitAttributesCommand.php | 1 + src/Console/Command/GitIgnoreCommand.php | 2 +- src/Console/Command/PhpDocCommand.php | 4 + src/Console/Command/RefactorCommand.php | 9 +- src/Console/Command/TestsCommand.php | 1 - src/Console/Command/WikiCommand.php | 4 + .../CommandLoader/DevToolsCommandLoader.php | 62 ++++++-- src/Console/DevTools.php | 7 +- src/License/PlaceholderResolver.php | 7 +- src/License/Reader.php | 12 +- src/Psr/Clock/SystemClock.php | 8 +- src/Psr/Container/Container.php | 35 ++++- tests/Agent/Skills/SkillsSynchronizerTest.php | 9 +- tests/Command/AbstractCommandTest.php | 128 --------------- tests/Command/CopyLicenseCommandTest.php | 15 +- tests/Command/DocsCommandTest.php | 11 +- tests/Command/GitAttributesCommandTest.php | 16 +- tests/Command/PhpDocCommandTest.php | 30 ++-- tests/Command/ReportsCommandTest.php | 18 --- tests/Command/TestsCommandTest.php | 4 + tests/Command/WikiCommandTest.php | 32 +++- .../DevToolsCommandProviderTest.php | 139 +++++++++-------- tests/Composer/Json/ComposerJsonTest.php | 134 ++++++++++++++++ .../DevToolsCommandLoaderTest.php | 75 +++++++++ tests/DevToolsTest.php | 55 ++++--- tests/License/PlaceholderResolverTest.php | 11 +- tests/License/ReaderTest.php | 47 ++++-- ...liNotifExecutionFinishedSubscriberTest.php | 117 ++++++++++++++ tests/Psr/Clock/SystemClockTest.php | 45 ++++++ tests/Psr/Container/ContainerTest.php | 147 ++++++++++++++++++ 39 files changed, 971 insertions(+), 361 deletions(-) delete mode 100644 tests/Command/AbstractCommandTest.php create mode 100644 tests/Composer/Json/ComposerJsonTest.php create mode 100644 tests/Console/CommandLoader/DevToolsCommandLoaderTest.php create mode 100644 tests/PhpUnit/Event/TestSuite/JoliNotifExecutionFinishedSubscriberTest.php create mode 100644 tests/Psr/Clock/SystemClockTest.php create mode 100644 tests/Psr/Container/ContainerTest.php diff --git a/bin/dev-tools.php b/bin/dev-tools.php index 2b24eef..268e1ec 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -18,6 +18,7 @@ namespace FastForward\DevTools; +use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Psr\Container\Container; use Symfony\Component\Console\Input\ArgvInput; @@ -26,5 +27,5 @@ require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -$application = Container::get(Console\DevTools::class); +$application = Container::get(DevTools::class); $application->run(new ArgvInput([...$argv, '--no-plugins'])); diff --git a/config/container.php b/config/container.php index ddd75f1..eaf0e3d 100644 --- a/config/container.php +++ b/config/container.php @@ -1,14 +1,53 @@ + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + namespace FastForward\DevTools; +use FastForward\DevTools\Psr\Clock\SystemClock; +use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; +use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; +use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; +use FastForward\DevTools\GitIgnore\MergerInterface; +use FastForward\DevTools\GitIgnore\Merger; +use FastForward\DevTools\GitIgnore\ReaderInterface; +use FastForward\DevTools\GitIgnore\Reader; +use FastForward\DevTools\GitIgnore\WriterInterface; +use FastForward\DevTools\GitIgnore\Writer; +use FastForward\DevTools\GitAttributes\CandidateProviderInterface; +use FastForward\DevTools\GitAttributes\CandidateProvider; +use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; +use FastForward\DevTools\GitAttributes\ExistenceChecker; +use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; +use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; +use FastForward\DevTools\License\GeneratorInterface; +use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\PlaceholderResolverInterface; +use FastForward\DevTools\License\PlaceholderResolver; +use FastForward\DevTools\License\ResolverInterface; +use FastForward\DevTools\License\Resolver; +use FastForward\DevTools\License\TemplateLoaderInterface; +use FastForward\DevTools\License\TemplateLoader; use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; -use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Finder\Finder; use function DI\create; @@ -20,32 +59,32 @@ // PSR LoggerInterface::class => get(NullLogger::class), - ClockInterface::class => get(Psr\Clock\SystemClock::class), + ClockInterface::class => get(SystemClock::class), // Console CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - CommandProvider::class => get(Composer\Capability\DevToolsCommandProvider::class), + CommandProvider::class => get(DevToolsCommandProvider::class), // Coverage - PhpUnit\Coverage\CoverageSummaryLoaderInterface::class => get(PhpUnit\Coverage\CoverageSummaryLoader::class), + CoverageSummaryLoaderInterface::class => get(CoverageSummaryLoader::class), // GitIgnore - GitIgnore\MergerInterface::class => get(GitIgnore\Merger::class), - GitIgnore\ReaderInterface::class => get(GitIgnore\Reader::class), - GitIgnore\WriterInterface::class => get(GitIgnore\Writer::class), + MergerInterface::class => get(Merger::class), + ReaderInterface::class => get(Reader::class), + WriterInterface::class => get(Writer::class), // GitAttributes - GitAttributes\CandidateProviderInterface::class => get(GitAttributes\CandidateProvider::class), - GitAttributes\ExistenceCheckerInterface::class => get(GitAttributes\ExistenceChecker::class), - GitAttributes\ExportIgnoreFilterInterface::class => get(GitAttributes\ExportIgnoreFilter::class), + CandidateProviderInterface::class => get(CandidateProvider::class), + ExistenceCheckerInterface::class => get(ExistenceChecker::class), + ExportIgnoreFilterInterface::class => get(ExportIgnoreFilter::class), GitAttributes\MergerInterface::class => get(GitAttributes\Merger::class), GitAttributes\ReaderInterface::class => get(GitAttributes\Reader::class), GitAttributes\WriterInterface::class => get(GitAttributes\Writer::class), // License - License\GeneratorInterface::class => get(License\Generator::class), - License\PlaceholderResolverInterface::class => get(License\PlaceholderResolver::class), + GeneratorInterface::class => get(Generator::class), + PlaceholderResolverInterface::class => get(PlaceholderResolver::class), License\ReaderInterface::class => get(License\Reader::class), - License\ResolverInterface::class => get(License\Resolver::class), - License\TemplateLoaderInterface::class => get(License\TemplateLoader::class), + ResolverInterface::class => get(Resolver::class), + TemplateLoaderInterface::class => get(TemplateLoader::class), ]; diff --git a/src/Agent/Skills/SkillsSynchronizer.php b/src/Agent/Skills/SkillsSynchronizer.php index 9486d9d..871f5bd 100644 --- a/src/Agent/Skills/SkillsSynchronizer.php +++ b/src/Agent/Skills/SkillsSynchronizer.php @@ -19,9 +19,7 @@ namespace FastForward\DevTools\Agent\Skills; use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Path; @@ -40,6 +38,7 @@ final class SkillsSynchronizer implements LoggerAwareInterface * * @param Filesystem $filesystem Filesystem instance for file operations * @param Finder $finder Finder instance for locating skill directories in the package + * @param LoggerInterface $logger */ public function __construct( private readonly Filesystem $filesystem, diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index f847183..36f61c2 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -18,6 +18,7 @@ namespace FastForward\DevTools\Composer\Capability; +use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Psr\Container\Container; @@ -33,6 +34,9 @@ final class DevToolsCommandProvider implements CommandProvider */ public function getCommands() { - return Container::get(DevTools::class)->getCommands(); + return array_values(array_filter( + Container::get(DevTools::class)->all(), + static fn(object $command): bool => $command instanceof BaseCommand, + )); } } diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index fe55b5b..2747c9d 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -1,5 +1,21 @@ + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + namespace FastForward\DevTools\Composer\Json; use Composer\Factory; @@ -9,22 +25,34 @@ final class ComposerJson extends JsonFile { private array $data; + /** + * @param string|null $path + */ public function __construct(?string $path = null) { parent::__construct($path ?? Factory::getComposerFile()); $this->data = $this->read(); } + /** + * @return string + */ public function getPackageName(): string { return $this->data['name'] ?? ''; } + /** + * @return string + */ public function getPackageDescription(): string { return $this->data['description'] ?? ''; } + /** + * @return string|null + */ public function getPackageLicense(): ?string { $license = $this->data['license'] ?? []; @@ -33,27 +61,38 @@ public function getPackageLicense(): ?string return $license; } - if (\is_array($license) && count($license) === 1) { + if (\is_array($license) && 1 === \count($license)) { return $license[0]; } return null; } + /** + * @return array + */ public function getAuthors(): array { return $this->data['authors'] ?? []; } + /** + * @return array + */ public function getExtra(): array { return $this->data['extra'] ?? []; } + /** + * @param string $type + * + * @return array + */ public function getAutoload(string $type = 'psr-4'): array { - $autoload = $this->data['autoload'] ?? []; + $autoload = $this->data['autoload'] ?? []; - return $autoload[$type] ?? []; + return $autoload[$type] ?? []; } } diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index b82796b..6afd653 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -36,7 +36,6 @@ */ abstract class AbstractCommand extends BaseCommand { - /** * Constructs a new AbstractCommand instance. * @@ -161,7 +160,7 @@ protected function getConfigFile(string $filename, bool $force = false): string */ protected function getDevToolsFile(string $filename): string { - return Path::makeAbsolute($filename, \dirname(__DIR__, 2)); + return Path::makeAbsolute($filename, \dirname(__DIR__, 3)); } /** diff --git a/src/Console/Command/CopyLicenseCommand.php b/src/Console/Command/CopyLicenseCommand.php index ae0f7e5..a2894b6 100644 --- a/src/Console/Command/CopyLicenseCommand.php +++ b/src/Console/Command/CopyLicenseCommand.php @@ -18,14 +18,7 @@ namespace FastForward\DevTools\Console\Command; -use Composer\Factory; -use FastForward\DevTools\License\Generator; use FastForward\DevTools\License\GeneratorInterface; -use FastForward\DevTools\License\PlaceholderResolver; -use FastForward\DevTools\License\Reader; -use FastForward\DevTools\License\Resolver; -use FastForward\DevTools\License\TemplateLoader; -use SplFileObject; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index 5ca0762..ff2eb27 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -28,12 +28,9 @@ * This command MUST report missing and unused dependencies using a single, * deterministic report that is friendly for local development and CI runs. */ -#[AsCommand( - name: 'dependencies', - description: 'Analyzes missing and unused Composer dependencies.', - help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.', - aliases: ['deps'] -)] +#[AsCommand(name: 'dependencies', description: 'Analyzes missing and unused Composer dependencies.', aliases: [ + 'deps', +], help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.')] final class DependenciesCommand extends AbstractCommand { /** diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index 83653ba..603529b 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -44,6 +44,10 @@ )] final class DocsCommand extends AbstractCommand { + /** + * @param ComposerJson $composerJson + * @param Filesystem $filesystem + */ public function __construct( private readonly ComposerJson $composerJson, Filesystem $filesystem diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index d2c7b6d..f1aa783 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -61,6 +61,7 @@ final class GitAttributesCommand extends AbstractCommand * @param ReaderInterface $reader the reader component * @param WriterInterface $writer the writer component * @param Filesystem $filesystem the filesystem component + * @param ComposerJson $composerJson */ public function __construct( private readonly CandidateProviderInterface $candidateProvider, diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index f8eec96..e4f3ee1 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -39,7 +39,7 @@ #[AsCommand( name: 'gitignore', description: 'Merges and synchronizes .gitignore files.', - help: 'This command merges the canonical .gitignore from dev-tools with the project\'s existing .gitignore.' + help: "This command merges the canonical .gitignore from dev-tools with the project's existing .gitignore." )] final class GitIgnoreCommand extends AbstractCommand { diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 111fdd9..9d8b49e 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -51,6 +51,10 @@ final class PhpDocCommand extends AbstractCommand */ public const string CONFIG = '.php-cs-fixer.dist.php'; + /** + * @param ComposerJson $composerJson + * @param Filesystem $filesystem + */ public function __construct( private readonly ComposerJson $composerJson, Filesystem $filesystem diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index e2768d5..31236a5 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -28,12 +28,9 @@ * 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.', - help: 'This command runs Rector to refactor your code.', - aliases: ['rector'] -)] +#[AsCommand(name: 'refactor', description: 'Runs Rector for code refactoring.', aliases: [ + 'rector', +], help: 'This command runs Rector to refactor your code.')] final class RefactorCommand extends AbstractCommand { /** diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index f50516b..91bc687 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools\Console\Command; use FastForward\DevTools\Composer\Json\ComposerJson; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; use InvalidArgumentException; use RuntimeException; diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 96a6269..2f63f2a 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -38,6 +38,10 @@ )] final class WikiCommand extends AbstractCommand { + /** + * @param ComposerJson $composerJson + * @param Filesystem $filesystem + */ public function __construct( private readonly ComposerJson $composerJson, Filesystem $filesystem diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index 16045f0..4949912 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -1,25 +1,48 @@ + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + namespace FastForward\DevTools\Console\CommandLoader; +use ReflectionClass; use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Finder\Finder; final class DevToolsCommandLoader extends ContainerCommandLoader { - public function __construct( - Finder $finder, - ContainerInterface $container, - ) { - parent::__construct($container, $this->getCommandMap($finder, $container)); + /** + * @param Finder $finder + * @param ContainerInterface $container + */ + public function __construct(Finder $finder, ContainerInterface $container) + { + parent::__construct($container, $this->getCommandMap($finder)); } - private function getCommandMap( - Finder $finder, - ContainerInterface $container - ): array { + /** + * @param Finder $finder + * + * @return array + */ + private function getCommandMap(Finder $finder): array + { $commandMap = []; $commandsDirectory = $finder @@ -27,16 +50,27 @@ private function getCommandMap( ->in(__DIR__ . '/../Command') ->name('*.php'); + $namespace = substr(__NAMESPACE__, 0, strrpos(__NAMESPACE__, '\\')) . '\\Command\\'; + foreach ($commandsDirectory as $file) { - $class = 'FastForward\\DevTools\\Console\\Command\\' . $file->getBasename('.php'); - $reflection = new \ReflectionClass($class); + $class = $namespace . $file->getBasename('.php'); + $reflection = new ReflectionClass($class); + if (! $reflection->isInstantiable()) { + continue; + } + + if (! $reflection->isSubclassOf(Command::class)) { + continue; + } + + $attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null; - if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(Command::class)) { + if (null === $attribute) { continue; } - $command = $container->get($class); - $commandMap[$command->getName()] = $class; + $arguments = $attribute->getArguments(); + $commandMap[$arguments['name']] = $class; } return $commandMap; diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index f631afe..ee39709 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -18,6 +18,7 @@ namespace FastForward\DevTools\Console; +use Override; use Composer\Console\Application as ComposerApplication; use ReflectionMethod; use Symfony\Component\Console\Application; @@ -37,9 +38,8 @@ final class DevTools extends ComposerApplication * * @param CommandLoaderInterface $commandLoader the command loader responsible for providing command instances */ - public function __construct( - CommandLoaderInterface $commandLoader, - ) { + public function __construct(CommandLoaderInterface $commandLoader) + { parent::__construct('Fast Forward Dev Tools'); $this->setDefaultCommand('standards'); @@ -54,6 +54,7 @@ public function __construct( * * @return array */ + #[Override] protected function getDefaultCommands(): array { $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); diff --git a/src/License/PlaceholderResolver.php b/src/License/PlaceholderResolver.php index 24bf4a1..a5b7435 100644 --- a/src/License/PlaceholderResolver.php +++ b/src/License/PlaceholderResolver.php @@ -29,10 +29,13 @@ * {{ organization }}, and {{ copyright_holder }} with values from metadata. * Unresolved placeholders are removed and excess newlines are normalized. */ -final class PlaceholderResolver implements PlaceholderResolverInterface +final readonly class PlaceholderResolver implements PlaceholderResolverInterface { + /** + * @param ClockInterface $clock + */ public function __construct( - private readonly ClockInterface $clock, + private ClockInterface $clock, ) {} /** diff --git a/src/License/Reader.php b/src/License/Reader.php index de6a72c..36f43cf 100644 --- a/src/License/Reader.php +++ b/src/License/Reader.php @@ -31,21 +31,19 @@ */ final readonly class Reader implements ReaderInterface { - private array $data; - /** * Creates a new Reader instance. * * @param ComposerJson $source The source file to read from, typically composer.json + * @param ClockInterface $clock + * @param ComposerJson $composerJson * * @throws JsonException if the JSON content is invalid */ public function __construct( - private readonly ClockInterface $clock, - private readonly ComposerJson $composerJson, - ) { - $this->data = $composerJson->read(); - } + private ClockInterface $clock, + private ComposerJson $composerJson + ) {} /** * Retrieves the license identifier from composer.json. diff --git a/src/Psr/Clock/SystemClock.php b/src/Psr/Clock/SystemClock.php index 88445dc..ab4b42f 100644 --- a/src/Psr/Clock/SystemClock.php +++ b/src/Psr/Clock/SystemClock.php @@ -18,12 +18,16 @@ namespace FastForward\DevTools\Psr\Clock; +use DateTimeImmutable; use Psr\Clock\ClockInterface; final class SystemClock implements ClockInterface { - public function now(): \DateTimeImmutable + /** + * @return DateTimeImmutable + */ + public function now(): DateTimeImmutable { - return new \DateTimeImmutable(); + return new DateTimeImmutable(); } } diff --git a/src/Psr/Container/Container.php b/src/Psr/Container/Container.php index da7e96e..05f13ea 100644 --- a/src/Psr/Container/Container.php +++ b/src/Psr/Container/Container.php @@ -1,5 +1,21 @@ + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + namespace FastForward\DevTools\Psr\Container; use DI\Container as DIContainer; @@ -7,13 +23,18 @@ final class Container { - private const CONFIG_FILE = __DIR__ . '/../../../config/container.php'; + private const string CONFIG_FILE = __DIR__ . '/../../../config/container.php'; private static ?ContainerInterface $container = null; + /** + * @param string $path + * + * @return ContainerInterface + */ private static function boot(string $path = self::CONFIG_FILE): ContainerInterface { - if (self::$container !== null) { + if (self::$container instanceof ContainerInterface) { return self::$container; } @@ -22,11 +43,21 @@ private static function boot(string $path = self::CONFIG_FILE): ContainerInterfa return self::$container = DIContainer::create($config); } + /** + * @param string $id + * + * @return mixed + */ public static function get(string $id): mixed { return self::boot()->get($id); } + /** + * @param string $id + * + * @return bool + */ public static function has(string $id): bool { diff --git a/tests/Agent/Skills/SkillsSynchronizerTest.php b/tests/Agent/Skills/SkillsSynchronizerTest.php index 3f13793..99f2557 100644 --- a/tests/Agent/Skills/SkillsSynchronizerTest.php +++ b/tests/Agent/Skills/SkillsSynchronizerTest.php @@ -262,9 +262,10 @@ private function createSkillDirectory(string $skillName, string $sourcePath): Sp */ private function createSynchronizer(): SkillsSynchronizer { - $synchronizer = new SkillsSynchronizer($this->filesystem->reveal(), $this->finder->reveal()); - $synchronizer->setLogger($this->logger->reveal()); - - return $synchronizer; + return new SkillsSynchronizer( + $this->filesystem->reveal(), + $this->finder->reveal(), + $this->logger->reveal(), + ); } } diff --git a/tests/Command/AbstractCommandTest.php b/tests/Command/AbstractCommandTest.php deleted file mode 100644 index 06f6cd2..0000000 --- a/tests/Command/AbstractCommandTest.php +++ /dev/null @@ -1,128 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Command; - -use FastForward\DevTools\Console\Command\AbstractCommand; -use FastForward\DevTools\Console\Command\AbstractCommand as AbstractCommandBase; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -#[CoversClass(AbstractCommand::class)] -final class AbstractCommandTest extends AbstractCommandTestCase -{ - /** - * @return AbstractCommand - */ - protected function getCommandClass(): AbstractCommand - { - return new AbstractCommandStub($this->filesystem->reveal()); - } - - /** - * @return string - */ - protected function getCommandName(): string - { - return 'stub'; - } - - /** - * @return string - */ - protected function getCommandDescription(): string - { - return 'Stub command for testing AbstractCommand.'; - } - - /** - * @return string - */ - protected function getCommandHelp(): string - { - return 'This is a stub command.'; - } - - /** - * @return void - */ - #[Test] - public function getAbsolutePathWillReturnAbsolutePathIfProvided(): void - { - $this->filesystem->isAbsolutePath('/absolute/path') - ->willReturn(true); - - self::assertSame('/absolute/path', $this->command->publicGetAbsolutePath('/absolute/path')); - } - - /** - * @return void - */ - #[Test] - public function getProjectNameWillReturnPackageNameFromComposer(): void - { - self::assertSame('fast-forward/dev-tools', $this->command->publicGetProjectName()); - } -} - -/** - * Stub class to test protected methods and base logic of AbstractCommand. - */ -class AbstractCommandStub extends AbstractCommand -{ - /** - * @return void - */ - protected function configure(): void - { - $this->setName('stub') - ->setDescription('Stub command for testing AbstractCommand.') - ->setHelp('This is a stub command.'); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - return self::SUCCESS; - } - - /** - * @param string $path - * - * @return string - */ - public function publicGetAbsolutePath(string $path): string - { - return $this->getAbsolutePath($path); - } - - /** - * @return string - */ - public function publicGetProjectName(): string - { - return $this->getProjectName(); - } -} diff --git a/tests/Command/CopyLicenseCommandTest.php b/tests/Command/CopyLicenseCommandTest.php index 1baf297..d742e81 100644 --- a/tests/Command/CopyLicenseCommandTest.php +++ b/tests/Command/CopyLicenseCommandTest.php @@ -28,7 +28,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; -use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -54,10 +53,7 @@ final class CopyLicenseCommandTest extends AbstractCommandTestCase */ protected function getCommandClass(): CopyLicenseCommand { - return new CopyLicenseCommand( - $this->generator->reveal(), - $this->filesystem->reveal() - ); + return new CopyLicenseCommand($this->generator->reveal(), $this->filesystem->reveal()); } /** @@ -101,8 +97,10 @@ protected function setUp(): void public function executeWillReturnSuccessAndWriteInfo(): void { $targetPath = getcwd() . '/LICENSE'; - $this->filesystem->exists($targetPath)->willReturn(false); - $this->generator->generate($targetPath)->willReturn(null); + $this->filesystem->exists($targetPath) + ->willReturn(false); + $this->generator->generate($targetPath) + ->willReturn(null); self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); } @@ -114,7 +112,8 @@ public function executeWillReturnSuccessAndWriteInfo(): void public function executeWillSkipWhenLicenseFileExists(): void { $targetPath = getcwd() . '/LICENSE'; - $this->filesystem->exists($targetPath)->willReturn(true); + $this->filesystem->exists($targetPath) + ->willReturn(true); self::assertSame(CopyLicenseCommand::SUCCESS, $this->invokeExecute()); } diff --git a/tests/Command/DocsCommandTest.php b/tests/Command/DocsCommandTest.php index 7a88370..3016678 100644 --- a/tests/Command/DocsCommandTest.php +++ b/tests/Command/DocsCommandTest.php @@ -44,10 +44,7 @@ final class DocsCommandTest extends AbstractCommandTestCase */ protected function getCommandClass(): DocsCommand { - return new DocsCommand( - $this->composerJson->reveal(), - $this->filesystem->reveal() - ); + return new DocsCommand($this->composerJson->reveal(), $this->filesystem->reveal()); } /** @@ -80,6 +77,12 @@ protected function getCommandHelp(): string protected function setUp(): void { $this->composerJson = $this->prophesize(ComposerJson::class); + $this->composerJson->getAutoload() + ->willReturn([ + 'FastForward\\DevTools\\' => getcwd() . '/src', + ]); + $this->composerJson->getPackageDescription() + ->willReturn('Fast Forward Dev Tools plugin'); parent::setUp(); } diff --git a/tests/Command/GitAttributesCommandTest.php b/tests/Command/GitAttributesCommandTest.php index 9471bba..ccc4be6 100644 --- a/tests/Command/GitAttributesCommandTest.php +++ b/tests/Command/GitAttributesCommandTest.php @@ -18,7 +18,8 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\GitAttributesCommand; +use FastForward\DevTools\Composer\Json\ComposerJson; +use FastForward\DevTools\Console\Command\GitAttributesCommand; use FastForward\DevTools\GitAttributes\CandidateProviderInterface; use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; @@ -65,6 +66,11 @@ final class GitAttributesCommandTest extends AbstractCommandTestCase */ private ObjectProphecy $writer; + /** + * @var ObjectProphecy + */ + private ObjectProphecy $composerJson; + /** * @return void */ @@ -76,6 +82,9 @@ protected function setUp(): void $this->merger = $this->prophesize(MergerInterface::class); $this->reader = $this->prophesize(ReaderInterface::class); $this->writer = $this->prophesize(WriterInterface::class); + $this->composerJson = $this->prophesize(ComposerJson::class); + $this->composerJson->getExtra() + ->willReturn([]); parent::setUp(); @@ -89,13 +98,14 @@ protected function setUp(): void protected function getCommandClass(): GitAttributesCommand { return new GitAttributesCommand( - $this->filesystem->reveal(), $this->candidateProvider->reveal(), $this->existenceChecker->reveal(), $this->exportIgnoreFilter->reveal(), $this->merger->reveal(), $this->reader->reveal(), $this->writer->reveal(), + $this->composerJson->reveal(), + $this->filesystem->reveal(), ); } @@ -177,7 +187,7 @@ public function executeWillRespectKeepInExportComposerConfiguration(): void $filteredFiles = ['/.editorconfig']; $entries = ['/docs/', '/.editorconfig']; - $this->package->getExtra() + $this->composerJson->getExtra() ->willReturn([ 'gitattributes' => [ 'keep-in-export' => $keepInExportPaths, diff --git a/tests/Command/PhpDocCommandTest.php b/tests/Command/PhpDocCommandTest.php index 5e983b7..80c5a88 100644 --- a/tests/Command/PhpDocCommandTest.php +++ b/tests/Command/PhpDocCommandTest.php @@ -18,13 +18,15 @@ namespace FastForward\DevTools\Tests\Command; -use Prophecy\Argument; -use RuntimeException; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\Console\Command\PhpDocCommand; use FastForward\DevTools\Console\Command\RefactorCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use RuntimeException; use function Safe\getcwd; @@ -34,11 +36,16 @@ final class PhpDocCommandTest extends AbstractCommandTestCase use ProphecyTrait; /** - * @return string + * @var ObjectProphecy */ - protected function getCommandClass(): string + private ObjectProphecy $composerJson; + + /** + * @return PhpDocCommand + */ + protected function getCommandClass(): PhpDocCommand { - return PhpDocCommand::class; + return new PhpDocCommand($this->composerJson->reveal(), $this->filesystem->reveal()); } /** @@ -70,6 +77,10 @@ protected function getCommandHelp(): string */ protected function setUp(): void { + $this->composerJson = $this->prophesize(ComposerJson::class); + $this->composerJson->getPackageName() + ->willReturn('fast-forward/dev-tools'); + parent::setUp(); $this->withConfigFile(PhpDocCommand::CONFIG); @@ -124,13 +135,8 @@ public function executeWillReturnFailureIfProcessFails(): void * @return void */ #[Test] - public function executeWillHandleComposerExceptionDuringDocheaderCreation(): void + public function executeWillSkipDocHeaderCreationWhenProjectDocHeaderAlreadyExists(): void { - // Mock getComposer to return null, which makes requireComposer return null (swallowed) - $this->application->getComposer() - ->willReturn(null); - - // Mock exists to return true for the project header so it returns early $this->filesystem->exists(getcwd() . '/' . PhpDocCommand::FILENAME)->willReturn(true); $this->willRunProcessWithCallback(static fn(): bool => true); @@ -138,5 +144,3 @@ public function executeWillHandleComposerExceptionDuringDocheaderCreation(): voi self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute()); } } - -// VDI diff --git a/tests/Command/ReportsCommandTest.php b/tests/Command/ReportsCommandTest.php index 72bccee..b07e831 100644 --- a/tests/Command/ReportsCommandTest.php +++ b/tests/Command/ReportsCommandTest.php @@ -23,7 +23,6 @@ use PHPUnit\Framework\Attributes\Test; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Console\Command\Command; #[CoversClass(ReportsCommand::class)] final class ReportsCommandTest extends AbstractCommandTestCase @@ -68,29 +67,12 @@ protected function getCommandHelp(): string #[Test] public function executeWillRunDocsAndTestsCommand(): void { - $docsCommand = $this->prophesize(Command::class); - $docsCommand->run(Argument::any(), Argument::any())->willReturn(ReportsCommand::SUCCESS); - $docsCommand->ignoreValidationErrors() - ->will(function (): void {}); - - $testsCommand = $this->prophesize(Command::class); - $testsCommand->run(Argument::any(), Argument::any())->willReturn(ReportsCommand::SUCCESS); - $testsCommand->ignoreValidationErrors() - ->will(function (): void {}); - - $this->application->find('docs') - ->willReturn($docsCommand->reveal()); - $this->application->find('tests') - ->willReturn($testsCommand->reveal()); - $this->output->writeln('Generating frontpage for Fast Forward documentation...') ->shouldBeCalled(); $this->output->writeln(Argument::containingString('Generating API documentation on path:')) ->shouldBeCalled(); $this->output->writeln(Argument::containingString('Generating test coverage report on path:')) ->shouldBeCalled(); - $this->output->writeln('Frontpage generation completed!') - ->shouldBeCalled(); self::assertSame(ReportsCommand::SUCCESS, $this->invokeExecute()); } diff --git a/tests/Command/TestsCommandTest.php b/tests/Command/TestsCommandTest.php index fd17c27..f95b1ae 100644 --- a/tests/Command/TestsCommandTest.php +++ b/tests/Command/TestsCommandTest.php @@ -91,6 +91,10 @@ protected function setUp(): void { $this->coverageSummaryLoader = $this->prophesize(CoverageSummaryLoaderInterface::class); $this->composerJson = $this->prophesize(ComposerJson::class); + $this->composerJson->getAutoload() + ->willReturn([ + 'FastForward\\DevTools\\' => 'src/', + ]); parent::setUp(); diff --git a/tests/Command/WikiCommandTest.php b/tests/Command/WikiCommandTest.php index 409a94d..2ce0166 100644 --- a/tests/Command/WikiCommandTest.php +++ b/tests/Command/WikiCommandTest.php @@ -18,10 +18,12 @@ namespace FastForward\DevTools\Tests\Command; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\Console\Command\WikiCommand; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Process\Process; #[CoversClass(WikiCommand::class)] @@ -30,11 +32,16 @@ final class WikiCommandTest extends AbstractCommandTestCase use ProphecyTrait; /** - * @return string + * @var ObjectProphecy + */ + private ObjectProphecy $composerJson; + + /** + * @return WikiCommand */ - protected function getCommandClass(): string + protected function getCommandClass(): WikiCommand { - return WikiCommand::class; + return new WikiCommand($this->composerJson->reveal(), $this->filesystem->reveal()); } /** @@ -58,7 +65,24 @@ protected function getCommandDescription(): string */ protected function getCommandHelp(): string { - return 'This command generates API documentation in Markdown format using phpDocumentor.'; + return 'This command generates API documentation in Markdown format using phpDocumentor. ' + . 'It accepts an optional `--target` option to specify the output directory for the generated documentation.'; + } + + /** + * @return void + */ + protected function setUp(): void + { + $this->composerJson = $this->prophesize(ComposerJson::class); + $this->composerJson->getPackageDescription() + ->willReturn('Fast Forward Dev Tools plugin'); + $this->composerJson->getAutoload() + ->willReturn([ + 'FastForward\\DevTools\\' => 'src/', + ]); + + parent::setUp(); } /** diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 6693b22..ab2a16f 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -18,62 +18,28 @@ namespace FastForward\DevTools\Tests\Composer\Capability; -use FastForward\DevTools\Console\Command\AbstractCommand; -use FastForward\DevTools\Console\Command\CodeStyleCommand; -use FastForward\DevTools\Console\Command\CopyLicenseCommand; -use FastForward\DevTools\Console\Command\DependenciesCommand; -use FastForward\DevTools\Console\Command\DocsCommand; -use FastForward\DevTools\Console\Command\GitAttributesCommand; -use FastForward\DevTools\Console\Command\GitIgnoreCommand; -use FastForward\DevTools\Console\Command\SyncCommand; -use FastForward\DevTools\Console\Command\SkillsCommand; -use FastForward\DevTools\Agent\Skills\SkillsSynchronizer; -use FastForward\DevTools\Console\Command\PhpDocCommand; -use FastForward\DevTools\Console\Command\RefactorCommand; -use FastForward\DevTools\Console\Command\ReportsCommand; -use FastForward\DevTools\Console\Command\StandardsCommand; -use FastForward\DevTools\Console\Command\TestsCommand; -use FastForward\DevTools\Console\Command\WikiCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\GitAttributes\CandidateProvider; -use FastForward\DevTools\GitAttributes\ExistenceChecker; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; -use FastForward\DevTools\GitAttributes\Merger as GitAttributesMerger; -use FastForward\DevTools\GitAttributes\Reader as GitAttributesReader; -use FastForward\DevTools\GitAttributes\Writer as GitAttributesWriter; -use FastForward\DevTools\GitIgnore\Merger as GitIgnoreMerger; -use FastForward\DevTools\GitIgnore\Writer; +use FastForward\DevTools\Console\Command\CodeStyleCommand; +use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Psr\Container\Container as StaticContainer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Container\ContainerInterface; +use ReflectionProperty; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Filesystem\Filesystem; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(CodeStyleCommand::class)] -#[UsesClass(RefactorCommand::class)] -#[UsesClass(TestsCommand::class)] -#[UsesClass(DependenciesCommand::class)] -#[UsesClass(PhpDocCommand::class)] -#[UsesClass(DocsCommand::class)] -#[UsesClass(StandardsCommand::class)] -#[UsesClass(ReportsCommand::class)] -#[UsesClass(WikiCommand::class)] -#[UsesClass(SyncCommand::class)] -#[UsesClass(GitIgnoreCommand::class)] -#[UsesClass(GitAttributesCommand::class)] -#[UsesClass(SkillsCommand::class)] -#[UsesClass(CopyLicenseCommand::class)] -#[UsesClass(SkillsSynchronizer::class)] -#[UsesClass(CandidateProvider::class)] -#[UsesClass(ExistenceChecker::class)] -#[UsesClass(ExportIgnoreFilter::class)] -#[UsesClass(GitAttributesMerger::class)] -#[UsesClass(GitAttributesReader::class)] -#[UsesClass(GitAttributesWriter::class)] -#[UsesClass(GitIgnoreMerger::class)] -#[UsesClass(Writer::class)] +#[UsesClass(DevTools::class)] +#[UsesClass(StaticContainer::class)] final class DevToolsCommandProviderTest extends TestCase { + use ProphecyTrait; + private DevToolsCommandProvider $commandProvider; /** @@ -82,43 +48,80 @@ final class DevToolsCommandProviderTest extends TestCase protected function setUp(): void { $this->commandProvider = new DevToolsCommandProvider(); + $this->setStaticContainer(null); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $this->setStaticContainer(null); } /** * @return void */ #[Test] - public function getCommandsWillReturnAllSupportedCommandsInExpectedOrder(): void + public function getCommandsWillReturnCommandsFromConfiguredDevToolsApplication(): void { - self::assertEquals( - [ - new CodeStyleCommand(), - new RefactorCommand(), - new TestsCommand(), - new DependenciesCommand(), - new PhpDocCommand(), - new DocsCommand(), - new StandardsCommand(), - new ReportsCommand(), - new WikiCommand(), - new SyncCommand(), - new GitIgnoreCommand(), - new GitAttributesCommand(), - new SkillsCommand(), - new CopyLicenseCommand(), - ], - $this->commandProvider->getCommands(), - ); + $customCommand = new CodeStyleCommand(new Filesystem()); + + $commandLoader = $this->prophesize(CommandLoaderInterface::class); + $commandLoader->getNames() + ->willReturn(['code-style']); + $commandLoader->has('code-style') + ->willReturn(true); + $commandLoader->get('code-style') + ->willReturn($customCommand); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(DevTools::class) + ->willReturn(new DevTools($commandLoader->reveal())) + ->shouldBeCalledOnce(); + + $this->setStaticContainer($container->reveal()); + + $commands = $this->commandProvider->getCommands(); + + self::assertCount(1, $commands); + self::assertSame($customCommand, $commands[0]); } /** * @return void */ #[Test] - public function getCommandsWillReturnOnlyAbstractCommandImplementations(): void + public function getCommandsWillReturnOnlyCommandInstances(): void { + $customCommand = new CodeStyleCommand(new Filesystem()); + + $commandLoader = $this->prophesize(CommandLoaderInterface::class); + $commandLoader->getNames() + ->willReturn(['code-style']); + $commandLoader->has('code-style') + ->willReturn(true); + $commandLoader->get('code-style') + ->willReturn($customCommand); + + $container = $this->prophesize(ContainerInterface::class); + $container->get(DevTools::class)->willReturn(new DevTools($commandLoader->reveal())); + + $this->setStaticContainer($container->reveal()); + foreach ($this->commandProvider->getCommands() as $command) { - self::assertInstanceOf(AbstractCommand::class, $command); + self::assertInstanceOf(CodeStyleCommand::class, $command); } } + + /** + * @param ContainerInterface|null $container + * + * @return void + */ + private function setStaticContainer(?ContainerInterface $container): void + { + $property = new ReflectionProperty(StaticContainer::class, 'container'); + $property->setValue(null, $container); + } } diff --git a/tests/Composer/Json/ComposerJsonTest.php b/tests/Composer/Json/ComposerJsonTest.php new file mode 100644 index 0000000..77e881b --- /dev/null +++ b/tests/Composer/Json/ComposerJsonTest.php @@ -0,0 +1,134 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Composer\Json; + +use FastForward\DevTools\Composer\Json\ComposerJson; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +use function Safe\file_put_contents; +use function Safe\json_encode; +use function Safe\tempnam; +use function Safe\unlink; + +#[CoversClass(ComposerJson::class)] +final class ComposerJsonTest extends TestCase +{ + /** + * @var list + */ + private array $temporaryFiles = []; + + /** + * @return void + */ + protected function tearDown(): void + { + foreach ($this->temporaryFiles as $temporaryFile) { + if (file_exists($temporaryFile)) { + unlink($temporaryFile); + } + } + } + + /** + * @return void + */ + #[Test] + public function accessorsWillReturnConfiguredComposerData(): void + { + $composerJson = $this->createComposerJson([ + 'name' => 'fast-forward/dev-tools', + 'description' => 'Fast Forward Development Tools for PHP projects', + 'license' => 'MIT', + 'authors' => [ + [ + 'name' => 'Felipe', + ], + ], + 'extra' => [ + 'gitattributes' => [ + 'keep-in-export' => ['/.github/'], + ], + ], + 'autoload' => [ + 'psr-4' => [ + 'FastForward\\DevTools\\' => 'src/', + ], + ], + ]); + + self::assertSame('fast-forward/dev-tools', $composerJson->getPackageName()); + self::assertSame('Fast Forward Development Tools for PHP projects', $composerJson->getPackageDescription()); + self::assertSame('MIT', $composerJson->getPackageLicense()); + self::assertSame([[ + 'name' => 'Felipe', + ]], $composerJson->getAuthors()); + self::assertSame([ + 'gitattributes' => [ + 'keep-in-export' => ['/.github/'], + ], + ], $composerJson->getExtra(),); + self::assertSame([ + 'FastForward\\DevTools\\' => 'src/', + ], $composerJson->getAutoload()); + } + + /** + * @return void + */ + #[Test] + public function getPackageLicenseWillReturnSingleLicenseFromArray(): void + { + $composerJson = $this->createComposerJson([ + 'license' => ['MIT'], + ]); + + self::assertSame('MIT', $composerJson->getPackageLicense()); + } + + /** + * @return void + */ + #[Test] + public function getPackageLicenseWillReturnNullForMultipleLicenses(): void + { + $composerJson = $this->createComposerJson([ + 'license' => ['MIT', 'Apache-2.0'], + ]); + + self::assertNull($composerJson->getPackageLicense()); + } + + /** + * @param array $contents + * + * @return ComposerJson + */ + private function createComposerJson(array $contents): ComposerJson + { + $temporaryFile = tempnam(sys_get_temp_dir(), 'composer-json-'); + $this->temporaryFiles[] = $temporaryFile; + + file_put_contents($temporaryFile, json_encode($contents, \JSON_THROW_ON_ERROR)); + + return new ComposerJson($temporaryFile); + } +} diff --git a/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php new file mode 100644 index 0000000..8697187 --- /dev/null +++ b/tests/Console/CommandLoader/DevToolsCommandLoaderTest.php @@ -0,0 +1,75 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Console\CommandLoader; + +use ArrayIterator; +use FastForward\DevTools\Console\Command\AbstractCommand; +use FastForward\DevTools\Console\Command\CodeStyleCommand; +use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Container\ContainerInterface; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; + +#[CoversClass(DevToolsCommandLoader::class)] +#[UsesClass(AbstractCommand::class)] +#[UsesClass(CodeStyleCommand::class)] +final class DevToolsCommandLoaderTest extends TestCase +{ + use ProphecyTrait; + + /** + * @return void + */ + #[Test] + public function constructorWillRegisterOnlyInstantiableCommands(): void + { + $commandDirectory = \dirname(__DIR__, 3) . '/src/Console/Command'; + $command = $this->prophesize(CodeStyleCommand::class); + + $finder = $this->prophesize(Finder::class); + $finder->files() + ->willReturn($finder->reveal()) + ->shouldBeCalled(); + $finder->in(Argument::type('string'))->willReturn($finder->reveal())->shouldBeCalled(); + $finder->name('*.php') + ->willReturn($finder->reveal()) + ->shouldBeCalled(); + $finder->getIterator() + ->willReturn(new ArrayIterator([ + new SplFileInfo($commandDirectory . '/AbstractCommand.php', '', 'AbstractCommand.php'), + new SplFileInfo($commandDirectory . '/CodeStyleCommand.php', '', 'CodeStyleCommand.php'), + ]))->shouldBeCalled(); + + $container = $this->prophesize(ContainerInterface::class); + $container->has(CodeStyleCommand::class)->willReturn(true)->shouldBeCalled(); + $container->get(CodeStyleCommand::class)->willReturn($command->reveal())->shouldBeCalled(); + + $loader = new DevToolsCommandLoader($finder->reveal(), $container->reveal()); + + self::assertFalse($loader->has('abstract')); + self::assertTrue($loader->has('code-style')); + self::assertSame($command->reveal(), $loader->get('code-style')); + } +} diff --git a/tests/DevToolsTest.php b/tests/DevToolsTest.php index 626b5a0..11819f0 100644 --- a/tests/DevToolsTest.php +++ b/tests/DevToolsTest.php @@ -18,8 +18,7 @@ namespace FastForward\DevTools\Tests; -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\DevTools; +use FastForward\DevTools\Console\DevTools; use Override; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -32,6 +31,7 @@ use Symfony\Component\Console\Command\DumpCompletionCommand; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; #[CoversClass(DevTools::class)] final class DevToolsTest extends TestCase @@ -39,9 +39,9 @@ final class DevToolsTest extends TestCase use ProphecyTrait; /** - * @var ObjectProphecy + * @var ObjectProphecy */ - private ObjectProphecy $commandProvider; + private ObjectProphecy $commandLoader; private DevTools $devTools; @@ -51,43 +51,50 @@ final class DevToolsTest extends TestCase #[Override] protected function setUp(): void { - $this->commandProvider = $this->prophesize(CommandProvider::class); - $this->devTools = new DevTools($this->commandProvider->reveal()); + $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); + $this->commandLoader->getNames() + ->willReturn([]); + $this->devTools = new DevTools($this->commandLoader->reveal()); } /** * @return void */ #[Test] - public function getDefaultCommandsWillMergeProvidedCommandsWithFrameworkDefaults(): void + public function getDefaultCommandsWillReturnFrameworkDefaultsOnly(): void { - $firstCommand = new class extends Command {}; - - $secondCommand = new class extends Command {}; - - $this->commandProvider - ->getCommands() - ->willReturn([$firstCommand, $secondCommand]) - ->shouldBeCalledOnce(); - $commands = $this->invokeGetDefaultCommands($this->devTools); - self::assertCount(6, $commands); - self::assertSame($firstCommand, $commands[0]); - self::assertSame($secondCommand, $commands[1]); - self::assertInstanceOf(HelpCommand::class, $commands[2]); - self::assertInstanceOf(ListCommand::class, $commands[3]); - self::assertInstanceOf(CompleteCommand::class, $commands[4]); - self::assertInstanceOf(DumpCompletionCommand::class, $commands[5]); + self::assertCount(4, $commands); + self::assertInstanceOf(HelpCommand::class, $commands[0]); + self::assertInstanceOf(ListCommand::class, $commands[1]); + self::assertInstanceOf(CompleteCommand::class, $commands[2]); + self::assertInstanceOf(DumpCompletionCommand::class, $commands[3]); } /** * @return void */ #[Test] - public function constructorWillSetApplicationName(): void + public function constructorWillSetApplicationNameAndExposeLoaderCommands(): void { + $customCommand = new class extends Command { + public function __construct() + { + parent::__construct('custom'); + } + }; + + $this->commandLoader->getNames() + ->willReturn(['custom']); + $this->commandLoader->has('custom') + ->willReturn(true); + $this->commandLoader->get('custom') + ->willReturn($customCommand); + self::assertSame('Fast Forward Dev Tools', $this->devTools->getName()); + self::assertTrue($this->devTools->has('custom')); + self::assertSame($customCommand, $this->devTools->get('custom')); } /** diff --git a/tests/License/PlaceholderResolverTest.php b/tests/License/PlaceholderResolverTest.php index 6c5b726..86833de 100644 --- a/tests/License/PlaceholderResolverTest.php +++ b/tests/License/PlaceholderResolverTest.php @@ -18,14 +18,19 @@ namespace FastForward\DevTools\Tests\License; +use DateTimeImmutable; use FastForward\DevTools\License\PlaceholderResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Clock\ClockInterface; #[CoversClass(PlaceholderResolver::class)] final class PlaceholderResolverTest extends TestCase { + use ProphecyTrait; + private PlaceholderResolver $resolver; /** @@ -35,7 +40,11 @@ protected function setUp(): void { parent::setUp(); - $this->resolver = new PlaceholderResolver(); + $clock = $this->prophesize(ClockInterface::class); + $clock->now() + ->willReturn(new DateTimeImmutable('2026-01-01 00:00:00')); + + $this->resolver = new PlaceholderResolver($clock->reveal()); } /** diff --git a/tests/License/ReaderTest.php b/tests/License/ReaderTest.php index a4907ab..16b2a1c 100644 --- a/tests/License/ReaderTest.php +++ b/tests/License/ReaderTest.php @@ -18,38 +18,61 @@ namespace FastForward\DevTools\Tests\License; +use DateTimeImmutable; +use FastForward\DevTools\Composer\Json\ComposerJson; use FastForward\DevTools\License\Reader; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use SplFileObject; +use Psr\Clock\ClockInterface; +use function Safe\file_put_contents; use function Safe\json_encode; +use function Safe\tempnam; +use function Safe\unlink; #[CoversClass(Reader::class)] +#[UsesClass(ComposerJson::class)] final class ReaderTest extends TestCase { use ProphecyTrait; + /** + * @var list + */ + private array $temporaryFiles = []; + /** * @param array $data * - * @return void + * @return Reader */ private function createReader(array $data): Reader { - $json = json_encode($data, \JSON_PRETTY_PRINT); + $temporaryFile = tempnam(sys_get_temp_dir(), 'composer-reader-'); + $this->temporaryFiles[] = $temporaryFile; + + file_put_contents($temporaryFile, json_encode($data, \JSON_PRETTY_PRINT)); - /** @var ObjectProphecy $file */ - $file = $this->prophesize(SplFileObject::class); - $file->getSize() - ->willReturn(\strlen($json)); - $file->fread(\strlen($json)) - ->willReturn($json); + $clock = $this->prophesize(ClockInterface::class); + $clock->now() + ->willReturn(new DateTimeImmutable('2026-01-01 00:00:00')); - return new Reader($file->reveal()); + return new Reader($clock->reveal(), new ComposerJson($temporaryFile)); + } + + /** + * @return void + */ + protected function tearDown(): void + { + foreach ($this->temporaryFiles as $temporaryFile) { + if (file_exists($temporaryFile)) { + unlink($temporaryFile); + } + } } /** @@ -192,6 +215,6 @@ public function getYearWillReturnCurrentYear(): void 'name' => 'fast-forward/dev-tools', ]); - self::assertSame((int) date('Y'), $reader->getYear()); + self::assertSame(2026, $reader->getYear()); } } diff --git a/tests/PhpUnit/Event/TestSuite/JoliNotifExecutionFinishedSubscriberTest.php b/tests/PhpUnit/Event/TestSuite/JoliNotifExecutionFinishedSubscriberTest.php new file mode 100644 index 0000000..1bcd46c --- /dev/null +++ b/tests/PhpUnit/Event/TestSuite/JoliNotifExecutionFinishedSubscriberTest.php @@ -0,0 +1,117 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\PhpUnit\Event\TestSuite; + +use FastForward\DevTools\PhpUnit\Event\EventTracer; +use FastForward\DevTools\PhpUnit\Event\TestSuite\JoliNotifExecutionFinishedSubscriber; +use Joli\JoliNotif\NotifierInterface; +use PHPUnit\Event\Test\Errored; +use PHPUnit\Event\Test\Failed; +use PHPUnit\Event\Test\Prepared; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use ReflectionMethod; + +#[CoversClass(JoliNotifExecutionFinishedSubscriber::class)] +#[UsesClass(EventTracer::class)] +final class JoliNotifExecutionFinishedSubscriberTest extends TestCase +{ + use ProphecyTrait; + + /** + * @return void + */ + #[Test] + public function getTitleWillReportSuccessWhenNoIssuesWereRecorded(): void + { + $subscriber = $this->createSubscriberWithCounts([ + Prepared::class => 3, + Failed::class => 0, + Errored::class => 0, + ]); + + self::assertSame('✅ Test Suite Passed', $this->invokePrivateMethod($subscriber, 'getTitle')); + } + + /** + * @return void + */ + #[Test] + public function getBodyWillSummarizePassedFailedAndErroredTests(): void + { + $subscriber = $this->createSubscriberWithCounts([ + Prepared::class => 5, + Failed::class => 1, + Errored::class => 2, + ]); + + self::assertSame( + "2 of 5 tests passed\n1 failure\n2 errors", + $this->invokePrivateMethod($subscriber, 'getBody'), + ); + } + + /** + * @return void + */ + #[Test] + public function getPassedTestsWillNeverReturnNegativeNumbers(): void + { + $subscriber = $this->createSubscriberWithCounts([ + Prepared::class => 1, + Failed::class => 3, + Errored::class => 1, + ]); + + self::assertSame(0, $this->invokePrivateMethod($subscriber, 'getPassedTests')); + } + + /** + * @param array $counts + * + * @return JoliNotifExecutionFinishedSubscriber + */ + private function createSubscriberWithCounts(array $counts): JoliNotifExecutionFinishedSubscriber + { + $tracer = $this->prophesize(EventTracer::class); + $tracer->count(Argument::type('string')) + ->will(static fn(array $arguments): int => $counts[$arguments[0]] ?? 0); + + $notifier = $this->prophesize(NotifierInterface::class); + + return new JoliNotifExecutionFinishedSubscriber($tracer->reveal(), $notifier->reveal()); + } + + /** + * @param object $subject + * @param string $method + * + * @return mixed + */ + private function invokePrivateMethod(object $subject, string $method): mixed + { + $reflectionMethod = new ReflectionMethod($subject, $method); + + return $reflectionMethod->invoke($subject); + } +} diff --git a/tests/Psr/Clock/SystemClockTest.php b/tests/Psr/Clock/SystemClockTest.php new file mode 100644 index 0000000..1043192 --- /dev/null +++ b/tests/Psr/Clock/SystemClockTest.php @@ -0,0 +1,45 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Psr\Clock; + +use DateTimeImmutable; +use FastForward\DevTools\Psr\Clock\SystemClock; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[CoversClass(SystemClock::class)] +final class SystemClockTest extends TestCase +{ + /** + * @return void + */ + #[Test] + public function nowWillReturnCurrentDateTimeImmutable(): void + { + $clock = new SystemClock(); + $before = new DateTimeImmutable(); + $now = $clock->now(); + $after = new DateTimeImmutable(); + + self::assertInstanceOf(DateTimeImmutable::class, $now); + self::assertGreaterThanOrEqual($before->getTimestamp(), $now->getTimestamp()); + self::assertLessThanOrEqual($after->getTimestamp(), $now->getTimestamp()); + } +} diff --git a/tests/Psr/Container/ContainerTest.php b/tests/Psr/Container/ContainerTest.php new file mode 100644 index 0000000..dbaea85 --- /dev/null +++ b/tests/Psr/Container/ContainerTest.php @@ -0,0 +1,147 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Psr\Container; + +use FastForward\DevTools\Psr\Container\Container; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Container\ContainerInterface; +use ReflectionMethod; +use ReflectionProperty; + +use function Safe\file_put_contents; +use function Safe\tempnam; +use function Safe\unlink; + +#[CoversClass(Container::class)] +final class ContainerTest extends TestCase +{ + use ProphecyTrait; + + /** + * @var list + */ + private array $temporaryFiles = []; + + /** + * @return void + */ + protected function setUp(): void + { + $this->setStaticContainer(null); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $this->setStaticContainer(null); + + foreach ($this->temporaryFiles as $temporaryFile) { + if (file_exists($temporaryFile)) { + unlink($temporaryFile); + } + } + } + + /** + * @return void + */ + #[Test] + public function getWillDelegateToTheBootedContainer(): void + { + $container = $this->prophesize(ContainerInterface::class); + $container->get('service') + ->willReturn('value') + ->shouldBeCalledOnce(); + + $this->setStaticContainer($container->reveal()); + + self::assertSame('value', Container::get('service')); + } + + /** + * @return void + */ + #[Test] + public function hasWillDelegateToTheBootedContainer(): void + { + $container = $this->prophesize(ContainerInterface::class); + $container->has('service') + ->willReturn(true) + ->shouldBeCalledOnce(); + + $this->setStaticContainer($container->reveal()); + + self::assertTrue(Container::has('service')); + } + + /** + * @return void + */ + #[Test] + public function bootWillCreateAndCacheAConfiguredPhpDiContainer(): void + { + $configurationFile = tempnam(sys_get_temp_dir(), 'container-config-'); + $this->temporaryFiles[] = $configurationFile; + + file_put_contents($configurationFile, <<<'PHP' + 'value', + ]; + PHP); + + /** @var ContainerInterface $container */ + $container = $this->invokePrivateBoot($configurationFile); + /** @var ContainerInterface $cachedContainer */ + $cachedContainer = $this->invokePrivateBoot($configurationFile); + + self::assertTrue($container->has('service')); + self::assertSame('value', $container->get('service')); + self::assertSame($container, $cachedContainer); + } + + /** + * @param string $configurationFile + * + * @return object + */ + private function invokePrivateBoot(string $configurationFile): object + { + $reflectionMethod = new ReflectionMethod(Container::class, 'boot'); + + return $reflectionMethod->invoke(null, $configurationFile); + } + + /** + * @param ContainerInterface|null $container + * + * @return void + */ + private function setStaticContainer(?ContainerInterface $container): void + { + $property = new ReflectionProperty(Container::class, 'container'); + $property->setValue(null, $container); + } +} From 770957429b2812244279cc6fcaaf73095e431ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:31:26 -0300 Subject: [PATCH 03/14] refactor: restructure command handling and implement service provider for dependency injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- bin/dev-tools.php | 4 +- composer.json | 1 + config/container.php | 90 ----------- .../Capability/DevToolsCommandProvider.php | 3 +- src/Console/DevTools.php | 20 +++ src/Psr/Container/Container.php | 66 -------- .../DevToolsServiceProvider.php | 94 +++++++++++ .../DevToolsCommandProviderTest.php | 95 ++++------- tests/{ => Console}/DevToolsTest.php | 2 +- tests/Psr/Container/ContainerTest.php | 147 ------------------ .../DevToolsServiceProviderTest.php | 48 ++++++ 11 files changed, 197 insertions(+), 373 deletions(-) delete mode 100644 config/container.php delete mode 100644 src/Psr/Container/Container.php create mode 100644 src/ServiceProvider/DevToolsServiceProvider.php rename tests/{ => Console}/DevToolsTest.php (98%) delete mode 100644 tests/Psr/Container/ContainerTest.php create mode 100644 tests/ServiceProvider/DevToolsServiceProviderTest.php diff --git a/bin/dev-tools.php b/bin/dev-tools.php index 268e1ec..cabafdf 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -19,7 +19,6 @@ namespace FastForward\DevTools; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Psr\Container\Container; use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; @@ -27,5 +26,4 @@ require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -$application = Container::get(DevTools::class); -$application->run(new ArgvInput([...$argv, '--no-plugins'])); +DevTools::create()->run(new ArgvInput([...$argv, '--no-plugins'])); diff --git a/composer.json b/composer.json index a776df3..b65b5c2 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "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/composer-normalize": "^2.50", "ergebnis/rector-rules": "^1.14", diff --git a/config/container.php b/config/container.php deleted file mode 100644 index eaf0e3d..0000000 --- a/config/container.php +++ /dev/null @@ -1,90 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools; - -use FastForward\DevTools\Psr\Clock\SystemClock; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; -use FastForward\DevTools\GitIgnore\MergerInterface; -use FastForward\DevTools\GitIgnore\Merger; -use FastForward\DevTools\GitIgnore\ReaderInterface; -use FastForward\DevTools\GitIgnore\Reader; -use FastForward\DevTools\GitIgnore\WriterInterface; -use FastForward\DevTools\GitIgnore\Writer; -use FastForward\DevTools\GitAttributes\CandidateProviderInterface; -use FastForward\DevTools\GitAttributes\CandidateProvider; -use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; -use FastForward\DevTools\GitAttributes\ExistenceChecker; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; -use FastForward\DevTools\License\GeneratorInterface; -use FastForward\DevTools\License\Generator; -use FastForward\DevTools\License\PlaceholderResolverInterface; -use FastForward\DevTools\License\PlaceholderResolver; -use FastForward\DevTools\License\ResolverInterface; -use FastForward\DevTools\License\Resolver; -use FastForward\DevTools\License\TemplateLoaderInterface; -use FastForward\DevTools\License\TemplateLoader; -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; -use Psr\Clock\ClockInterface; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; -use Symfony\Component\Finder\Finder; - -use function DI\create; -use function DI\get; - -return [ - // Symfony Components - Finder::class => create(Finder::class), - - // PSR - LoggerInterface::class => get(NullLogger::class), - ClockInterface::class => get(SystemClock::class), - - // Console - CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - CommandProvider::class => get(DevToolsCommandProvider::class), - - // Coverage - CoverageSummaryLoaderInterface::class => get(CoverageSummaryLoader::class), - - // GitIgnore - MergerInterface::class => get(Merger::class), - ReaderInterface::class => get(Reader::class), - WriterInterface::class => get(Writer::class), - - // GitAttributes - CandidateProviderInterface::class => get(CandidateProvider::class), - ExistenceCheckerInterface::class => get(ExistenceChecker::class), - ExportIgnoreFilterInterface::class => get(ExportIgnoreFilter::class), - GitAttributes\MergerInterface::class => get(GitAttributes\Merger::class), - GitAttributes\ReaderInterface::class => get(GitAttributes\Reader::class), - GitAttributes\WriterInterface::class => get(GitAttributes\Writer::class), - - // License - GeneratorInterface::class => get(Generator::class), - PlaceholderResolverInterface::class => get(PlaceholderResolver::class), - License\ReaderInterface::class => get(License\Reader::class), - ResolverInterface::class => get(Resolver::class), - TemplateLoaderInterface::class => get(TemplateLoader::class), -]; diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index 36f61c2..737475e 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -21,7 +21,6 @@ use Composer\Command\BaseCommand; use Composer\Plugin\Capability\CommandProvider; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Psr\Container\Container; /** * Provides a registry of custom dev-tools commands mapped for Composer integration. @@ -35,7 +34,7 @@ final class DevToolsCommandProvider implements CommandProvider public function getCommands() { return array_values(array_filter( - Container::get(DevTools::class)->all(), + DevTools::create()->all(), static fn(object $command): bool => $command instanceof BaseCommand, )); } diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index ee39709..0042d7f 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -18,8 +18,11 @@ namespace FastForward\DevTools\Console; +use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use Override; use Composer\Console\Application as ComposerApplication; +use DI\Container; +use Psr\Container\ContainerInterface; use ReflectionMethod; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -30,6 +33,8 @@ */ final class DevTools extends ComposerApplication { + private static ?ContainerInterface $container = null; + /** * Initializes the DevTools global context and dependency graph. * @@ -46,6 +51,21 @@ public function __construct(CommandLoaderInterface $commandLoader) $this->setCommandLoader($commandLoader); } + /** + * Create DevTools instance from container. + * + * @return DevTools + */ + public static function create(): self + { + if (self::$container === null) { + $serviceProvider = new DevToolsServiceProvider(); + self::$container = new Container($serviceProvider->getFactories()); + } + + return self::$container->get(self::class); + } + /** * Retrieves the default set of commands provided by the Symfony Application. * diff --git a/src/Psr/Container/Container.php b/src/Psr/Container/Container.php deleted file mode 100644 index 05f13ea..0000000 --- a/src/Psr/Container/Container.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Psr\Container; - -use DI\Container as DIContainer; -use Psr\Container\ContainerInterface; - -final class Container -{ - private const string CONFIG_FILE = __DIR__ . '/../../../config/container.php'; - - private static ?ContainerInterface $container = null; - - /** - * @param string $path - * - * @return ContainerInterface - */ - private static function boot(string $path = self::CONFIG_FILE): ContainerInterface - { - if (self::$container instanceof ContainerInterface) { - return self::$container; - } - - $config = require_once $path; - - return self::$container = DIContainer::create($config); - } - - /** - * @param string $id - * - * @return mixed - */ - public static function get(string $id): mixed - { - return self::boot()->get($id); - } - - /** - * @param string $id - * - * @return bool - */ - public static function has(string $id): bool - { - - return self::boot()->has($id); - } -} diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php new file mode 100644 index 0000000..eb47d2c --- /dev/null +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -0,0 +1,94 @@ + create(Finder::class), + + // PSR + LoggerInterface::class => get(NullLogger::class), + ClockInterface::class => get(SystemClock::class), + + // Console + CommandLoaderInterface::class => get(DevToolsCommandLoader::class), + CommandProvider::class => get(DevToolsCommandProvider::class), + + // Coverage + CoverageSummaryLoaderInterface::class => get(CoverageSummaryLoader::class), + + // GitIgnore + MergerInterface::class => get(Merger::class), + ReaderInterface::class => get(Reader::class), + WriterInterface::class => get(Writer::class), + + // GitAttributes + CandidateProviderInterface::class => get(CandidateProvider::class), + ExistenceCheckerInterface::class => get(ExistenceChecker::class), + ExportIgnoreFilterInterface::class => get(ExportIgnoreFilter::class), + GitAttributesMergerInterface::class => get(GitAttributesMerger::class), + GitAttributesReaderInterface::class => get(GitAttributesReader::class), + GitAttributesWriterInterface::class => get(GitAttributesWriter::class), + + // License + GeneratorInterface::class => get(Generator::class), + PlaceholderResolverInterface::class => get(PlaceholderResolver::class), + LicenseReaderInterface::class => get(LicenseReader::class), + ResolverInterface::class => get(Resolver::class), + TemplateLoaderInterface::class => get(TemplateLoader::class), + ]; + } + + public function getExtensions(): array + { + return []; + } +} diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index ab2a16f..a52569b 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -18,28 +18,31 @@ namespace FastForward\DevTools\Tests\Composer\Capability; +use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; use FastForward\DevTools\Console\Command\CodeStyleCommand; use FastForward\DevTools\Console\DevTools; -use FastForward\DevTools\Psr\Container\Container as StaticContainer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use ReflectionProperty; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Filesystem\Filesystem; #[CoversClass(DevToolsCommandProvider::class)] -#[UsesClass(CodeStyleCommand::class)] #[UsesClass(DevTools::class)] -#[UsesClass(StaticContainer::class)] final class DevToolsCommandProviderTest extends TestCase { use ProphecyTrait; + private ObjectProphecy $container; + private ObjectProphecy $devTools; + private DevToolsCommandProvider $commandProvider; /** @@ -47,81 +50,45 @@ final class DevToolsCommandProviderTest extends TestCase */ protected function setUp(): void { + $this->container = $this->prophesize(ContainerInterface::class); + $this->devTools = $this->prophesize(DevTools::class); + + $this->container->get(DevTools::class) + ->willReturn($this->devTools->reveal()) + ->shouldBeCalledOnce(); + + $this->devTools->all()->willReturn([])->shouldBeCalledOnce(); + $this->commandProvider = new DevToolsCommandProvider(); - $this->setStaticContainer(null); - } - /** - * @return void - */ - protected function tearDown(): void - { - $this->setStaticContainer(null); + $property = new ReflectionProperty(DevTools::class, 'container'); + $property->setValue(null, $this->container->reveal()); } - /** - * @return void - */ #[Test] - public function getCommandsWillReturnCommandsFromConfiguredDevToolsApplication(): void + public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): void { - $customCommand = new CodeStyleCommand(new Filesystem()); - - $commandLoader = $this->prophesize(CommandLoaderInterface::class); - $commandLoader->getNames() - ->willReturn(['code-style']); - $commandLoader->has('code-style') - ->willReturn(true); - $commandLoader->get('code-style') - ->willReturn($customCommand); - - $container = $this->prophesize(ContainerInterface::class); - $container->get(DevTools::class) - ->willReturn(new DevTools($commandLoader->reveal())) - ->shouldBeCalledOnce(); - - $this->setStaticContainer($container->reveal()); - $commands = $this->commandProvider->getCommands(); - self::assertCount(1, $commands); - self::assertSame($customCommand, $commands[0]); + self::assertIsArray($commands); + self::assertEmpty($commands); } - /** - * @return void - */ #[Test] - public function getCommandsWillReturnOnlyCommandInstances(): void + public function getCommandsWillReturnRegisteredBaseCommands(): void { - $customCommand = new CodeStyleCommand(new Filesystem()); - - $commandLoader = $this->prophesize(CommandLoaderInterface::class); - $commandLoader->getNames() - ->willReturn(['code-style']); - $commandLoader->has('code-style') - ->willReturn(true); - $commandLoader->get('code-style') - ->willReturn($customCommand); - - $container = $this->prophesize(ContainerInterface::class); - $container->get(DevTools::class)->willReturn(new DevTools($commandLoader->reveal())); + $composerCommand = $this->prophesize(BaseCommand::class)->reveal(); + $symfonyCommand = $this->prophesize(Command::class)->reveal(); - $this->setStaticContainer($container->reveal()); + $this->devTools->all()->willReturn([ + $composerCommand, + $symfonyCommand, + ])->shouldBeCalledOnce(); - foreach ($this->commandProvider->getCommands() as $command) { - self::assertInstanceOf(CodeStyleCommand::class, $command); - } - } + $commands = $this->commandProvider->getCommands(); - /** - * @param ContainerInterface|null $container - * - * @return void - */ - private function setStaticContainer(?ContainerInterface $container): void - { - $property = new ReflectionProperty(StaticContainer::class, 'container'); - $property->setValue(null, $container); + self::assertIsArray($commands); + self::assertCount(1, $commands); + self::assertSame($composerCommand, $commands[0]); } } diff --git a/tests/DevToolsTest.php b/tests/Console/DevToolsTest.php similarity index 98% rename from tests/DevToolsTest.php rename to tests/Console/DevToolsTest.php index 11819f0..ac20b32 100644 --- a/tests/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -16,7 +16,7 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Tests; +namespace FastForward\DevTools\Tests\Console; use FastForward\DevTools\Console\DevTools; use Override; diff --git a/tests/Psr/Container/ContainerTest.php b/tests/Psr/Container/ContainerTest.php deleted file mode 100644 index dbaea85..0000000 --- a/tests/Psr/Container/ContainerTest.php +++ /dev/null @@ -1,147 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\Tests\Psr\Container; - -use FastForward\DevTools\Psr\Container\Container; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Container\ContainerInterface; -use ReflectionMethod; -use ReflectionProperty; - -use function Safe\file_put_contents; -use function Safe\tempnam; -use function Safe\unlink; - -#[CoversClass(Container::class)] -final class ContainerTest extends TestCase -{ - use ProphecyTrait; - - /** - * @var list - */ - private array $temporaryFiles = []; - - /** - * @return void - */ - protected function setUp(): void - { - $this->setStaticContainer(null); - } - - /** - * @return void - */ - protected function tearDown(): void - { - $this->setStaticContainer(null); - - foreach ($this->temporaryFiles as $temporaryFile) { - if (file_exists($temporaryFile)) { - unlink($temporaryFile); - } - } - } - - /** - * @return void - */ - #[Test] - public function getWillDelegateToTheBootedContainer(): void - { - $container = $this->prophesize(ContainerInterface::class); - $container->get('service') - ->willReturn('value') - ->shouldBeCalledOnce(); - - $this->setStaticContainer($container->reveal()); - - self::assertSame('value', Container::get('service')); - } - - /** - * @return void - */ - #[Test] - public function hasWillDelegateToTheBootedContainer(): void - { - $container = $this->prophesize(ContainerInterface::class); - $container->has('service') - ->willReturn(true) - ->shouldBeCalledOnce(); - - $this->setStaticContainer($container->reveal()); - - self::assertTrue(Container::has('service')); - } - - /** - * @return void - */ - #[Test] - public function bootWillCreateAndCacheAConfiguredPhpDiContainer(): void - { - $configurationFile = tempnam(sys_get_temp_dir(), 'container-config-'); - $this->temporaryFiles[] = $configurationFile; - - file_put_contents($configurationFile, <<<'PHP' - 'value', - ]; - PHP); - - /** @var ContainerInterface $container */ - $container = $this->invokePrivateBoot($configurationFile); - /** @var ContainerInterface $cachedContainer */ - $cachedContainer = $this->invokePrivateBoot($configurationFile); - - self::assertTrue($container->has('service')); - self::assertSame('value', $container->get('service')); - self::assertSame($container, $cachedContainer); - } - - /** - * @param string $configurationFile - * - * @return object - */ - private function invokePrivateBoot(string $configurationFile): object - { - $reflectionMethod = new ReflectionMethod(Container::class, 'boot'); - - return $reflectionMethod->invoke(null, $configurationFile); - } - - /** - * @param ContainerInterface|null $container - * - * @return void - */ - private function setStaticContainer(?ContainerInterface $container): void - { - $property = new ReflectionProperty(Container::class, 'container'); - $property->setValue(null, $container); - } -} diff --git a/tests/ServiceProvider/DevToolsServiceProviderTest.php b/tests/ServiceProvider/DevToolsServiceProviderTest.php new file mode 100644 index 0000000..846b85c --- /dev/null +++ b/tests/ServiceProvider/DevToolsServiceProviderTest.php @@ -0,0 +1,48 @@ + + * @license https://opensource.org/licenses/MIT MIT License + */ + +namespace FastForward\DevTools\Tests\ServiceProvider; + +use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Interop\Container\ServiceProviderInterface; + +#[CoversClass(DevToolsServiceProvider::class)] +final class DevToolsServiceProviderTest extends TestCase +{ + private DevToolsServiceProvider $provider; + + protected function setUp(): void + { + $this->provider = new DevToolsServiceProvider(); + } + + #[Test] + public function implementsServiceProviderInterface(): void + { + self::assertInstanceOf(ServiceProviderInterface::class, $this->provider); + } + + #[Test] + public function getExtensionsReturnEmptyArray(): void + { + self::assertEmpty($this->provider->getExtensions()); + } + + #[Test] + public function getFactoriesReturnFactories(): void + { + $factories = $this->provider->getFactories(); + + self::assertIsArray($factories); + self::assertNotEmpty($factories); + } +} From 36763dddf6bc4003a7e62d80e13bf050488739f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:32:17 -0300 Subject: [PATCH 04/14] refactor: improve container initialization check and enhance DevToolsServiceProvider documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/DevTools.php | 2 +- .../DevToolsServiceProvider.php | 22 +++++++++++++++++++ .../DevToolsCommandProviderTest.php | 19 +++++++++------- .../DevToolsServiceProviderTest.php | 21 ++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 0042d7f..092b309 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -58,7 +58,7 @@ public function __construct(CommandLoaderInterface $commandLoader) */ public static function create(): self { - if (self::$container === null) { + if (! self::$container instanceof ContainerInterface) { $serviceProvider = new DevToolsServiceProvider(); self::$container = new Container($serviceProvider->getFactories()); } diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index eb47d2c..7e97ff9 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -1,5 +1,21 @@ + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + namespace FastForward\DevTools\ServiceProvider; use Interop\Container\ServiceProviderInterface; @@ -48,6 +64,9 @@ final class DevToolsServiceProvider implements ServiceProviderInterface { + /** + * @return array + */ public function getFactories(): array { return [ @@ -87,6 +106,9 @@ public function getFactories(): array ]; } + /** + * @return array + */ public function getExtensions(): array { return []; diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index a52569b..1d2e9f0 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -20,7 +20,6 @@ use Composer\Command\BaseCommand; use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\Console\Command\CodeStyleCommand; use FastForward\DevTools\Console\DevTools; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -31,8 +30,6 @@ use Psr\Container\ContainerInterface; use ReflectionProperty; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; -use Symfony\Component\Filesystem\Filesystem; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(DevTools::class)] @@ -41,6 +38,7 @@ final class DevToolsCommandProviderTest extends TestCase use ProphecyTrait; private ObjectProphecy $container; + private ObjectProphecy $devTools; private DevToolsCommandProvider $commandProvider; @@ -57,7 +55,8 @@ protected function setUp(): void ->willReturn($this->devTools->reveal()) ->shouldBeCalledOnce(); - $this->devTools->all()->willReturn([])->shouldBeCalledOnce(); + $this->devTools->all() + ->willReturn([])->shouldBeCalledOnce(); $this->commandProvider = new DevToolsCommandProvider(); @@ -65,6 +64,9 @@ protected function setUp(): void $property->setValue(null, $this->container->reveal()); } + /** + * @return void + */ #[Test] public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): void { @@ -74,16 +76,17 @@ public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): vo self::assertEmpty($commands); } + /** + * @return void + */ #[Test] public function getCommandsWillReturnRegisteredBaseCommands(): void { $composerCommand = $this->prophesize(BaseCommand::class)->reveal(); $symfonyCommand = $this->prophesize(Command::class)->reveal(); - $this->devTools->all()->willReturn([ - $composerCommand, - $symfonyCommand, - ])->shouldBeCalledOnce(); + $this->devTools->all() + ->willReturn([$composerCommand, $symfonyCommand])->shouldBeCalledOnce(); $commands = $this->commandProvider->getCommands(); diff --git a/tests/ServiceProvider/DevToolsServiceProviderTest.php b/tests/ServiceProvider/DevToolsServiceProviderTest.php index 846b85c..d1ac7af 100644 --- a/tests/ServiceProvider/DevToolsServiceProviderTest.php +++ b/tests/ServiceProvider/DevToolsServiceProviderTest.php @@ -3,8 +3,17 @@ declare(strict_types=1); /** + * This file is part of fast-forward/dev-tools. + * + * This source file is subject to the license bundled + * with this source code in the file LICENSE. + * * @copyright Copyright (c) 2026 Felipe Sayão Lobato Abreu * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward + * @see https://datatracker.ietf.org/doc/html/rfc2119 */ namespace FastForward\DevTools\Tests\ServiceProvider; @@ -20,23 +29,35 @@ final class DevToolsServiceProviderTest extends TestCase { private DevToolsServiceProvider $provider; + /** + * @return void + */ protected function setUp(): void { $this->provider = new DevToolsServiceProvider(); } + /** + * @return void + */ #[Test] public function implementsServiceProviderInterface(): void { self::assertInstanceOf(ServiceProviderInterface::class, $this->provider); } + /** + * @return void + */ #[Test] public function getExtensionsReturnEmptyArray(): void { self::assertEmpty($this->provider->getExtensions()); } + /** + * @return void + */ #[Test] public function getFactoriesReturnFactories(): void { From 3781355628487007283c8b3770fc9e4a8b7bfb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:39:45 -0300 Subject: [PATCH 05/14] refactor: enhance documentation for ComposerJson, DevToolsCommandLoader, SystemClock, and DevToolsServiceProvider classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Composer/Json/ComposerJson.php | 89 +++++++++++++++++-- .../CommandLoader/DevToolsCommandLoader.php | 8 ++ src/Console/DevTools.php | 3 + src/Psr/Clock/SystemClock.php | 7 ++ .../DevToolsServiceProvider.php | 6 ++ 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php index 2747c9d..18200e3 100644 --- a/src/Composer/Json/ComposerJson.php +++ b/src/Composer/Json/ComposerJson.php @@ -21,12 +21,41 @@ use Composer\Factory; use Composer\Json\JsonFile; +/** + * Represents a specialized reader for a Composer JSON file. + * + * This class SHALL provide convenient accessors for commonly used + * `composer.json` metadata after reading and caching the file contents. + * Consumers SHOULD use this class when they need normalized access to + * package-level metadata. The internal data cache MUST reflect the + * contents returned by the underlying JSON file reader at construction + * time. + */ final class ComposerJson extends JsonFile { + /** + * Stores the decoded Composer JSON document contents. + * + * This property MUST contain the data read from the target Composer + * file during construction. Consumers SHOULD treat the structure as + * internal implementation detail and SHALL rely on accessor methods + * instead of direct access. + * + * @var array + */ private array $data; /** - * @param string|null $path + * Initializes the Composer JSON reader. + * + * When no path is provided, the default Composer file location + * returned by Composer's factory SHALL be used. The constructor MUST + * immediately read and cache the JSON document contents so that + * subsequent accessor methods can operate on the in-memory data. + * + * @param string|null $path The absolute or relative path to a + * Composer JSON file. When omitted, the + * default Composer file path SHALL be used. */ public function __construct(?string $path = null) { @@ -35,7 +64,13 @@ public function __construct(?string $path = null) } /** - * @return string + * Returns the package name declared in the Composer file. + * + * This method SHALL return the value of the `name` key when present. + * If the package name is not defined, the method MUST return an + * empty string. + * + * @return string the package name, or an empty string when undefined */ public function getPackageName(): string { @@ -43,7 +78,14 @@ public function getPackageName(): string } /** - * @return string + * Returns the package description declared in the Composer file. + * + * This method SHALL return the value of the `description` key when + * present. If the description is not defined, the method MUST return + * an empty string. + * + * @return string the package description, or an empty string when + * undefined */ public function getPackageDescription(): string { @@ -51,7 +93,16 @@ public function getPackageDescription(): string } /** - * @return string|null + * Returns the package license when it can be resolved to a single value. + * + * This method SHALL return the `license` value directly when it is a + * string. When the license is an array containing exactly one item, + * that single item SHALL be returned. When the license field is not + * present, is empty, or cannot be resolved to exactly one string + * value, the method MUST return null. + * + * @return string|null the resolved license identifier, or null when + * no single license value can be determined */ public function getPackageLicense(): ?string { @@ -69,7 +120,14 @@ public function getPackageLicense(): ?string } /** - * @return array + * Returns the package authors declared in the Composer file. + * + * This method SHALL return the value of the `authors` key when + * present. If the key is absent, the method MUST return an empty + * array. + * + * @return array the authors list as declared in the Composer file, + * or an empty array when undefined */ public function getAuthors(): array { @@ -77,7 +135,13 @@ public function getAuthors(): array } /** - * @return array + * Returns the extra configuration section declared in the Composer file. + * + * This method SHALL return the value of the `extra` key when present. + * If the key is absent, the method MUST return an empty array. + * + * @return array the extra configuration data, or an empty array when + * undefined */ public function getExtra(): array { @@ -85,9 +149,18 @@ public function getExtra(): array } /** - * @param string $type + * Returns the autoload configuration for the requested autoload type. + * + * This method SHALL inspect the `autoload` section and return the + * nested configuration for the requested type, such as `psr-4`. + * When the `autoload` section or the requested type is not defined, + * the method MUST return an empty array. + * + * @param string $type The autoload mapping type to retrieve. This + * defaults to `psr-4`. * - * @return array + * @return array the autoload configuration for the requested type, + * or an empty array when unavailable */ public function getAutoload(string $type = 'psr-4'): array { diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index 4949912..b1fa506 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -28,6 +28,12 @@ final class DevToolsCommandLoader extends ContainerCommandLoader { /** + * Constructs the DevToolsCommandLoader. + * + * This constructor initializes the command loader by scanning the Command directory for classes that are + * instantiable and have the AsCommand attribute. + * It builds a command map associating command names with their respective classes. + * * @param Finder $finder * @param ContainerInterface $container */ @@ -37,6 +43,8 @@ public function __construct(Finder $finder, ContainerInterface $container) } /** + * Builds a command map by scanning the Command directory for classes that are instantiable and have the AsCommand attribute. + * * @param Finder $finder * * @return array diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 092b309..e3c7c3c 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -33,6 +33,9 @@ */ final class DevTools extends ComposerApplication { + /** + * @var ContainerInterface holds the static container instance for global access within the DevTools context + */ private static ?ContainerInterface $container = null; /** diff --git a/src/Psr/Clock/SystemClock.php b/src/Psr/Clock/SystemClock.php index ab4b42f..1d67ebf 100644 --- a/src/Psr/Clock/SystemClock.php +++ b/src/Psr/Clock/SystemClock.php @@ -21,9 +21,16 @@ use DateTimeImmutable; use Psr\Clock\ClockInterface; +/** + * A clock implementation that returns the current system time. + * + * This class implements the ClockInterface and provides a method to get the current time as a DateTimeImmutable object. + */ final class SystemClock implements ClockInterface { /** + * Returns the current time as a DateTimeImmutable Object. + * * @return DateTimeImmutable */ public function now(): DateTimeImmutable diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index 7e97ff9..d808651 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -62,6 +62,12 @@ use function DI\create; use function DI\get; +/** + * DevToolsServiceProvider registers the services provided by this package. + * + * This class implements the ServiceProviderInterface from the PHP-Interop container package, + * allowing it to be used with any compatible dependency injection container. + */ final class DevToolsServiceProvider implements ServiceProviderInterface { /** From 86693a2b77844147cb295e0e9493ec1bc4ca1c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:55:51 -0300 Subject: [PATCH 06/14] refactor: format AsCommand attributes for DependenciesCommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/DependenciesCommand.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index ff2eb27..7e5186c 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -28,9 +28,12 @@ * This command MUST report missing and unused dependencies using a single, * deterministic report that is friendly for local development and CI runs. */ -#[AsCommand(name: 'dependencies', description: 'Analyzes missing and unused Composer dependencies.', aliases: [ - 'deps', -], help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.')] +#[AsCommand( + name: 'dependencies', + description: 'Analyzes missing and unused Composer dependencies.', + aliases: ['deps'], + help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.' +)] final class DependenciesCommand extends AbstractCommand { /** From 022683a61e0bb54a49292ad2d4aee789831f8d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:56:50 -0300 Subject: [PATCH 07/14] refactor: improve documentation for ComposerJson parameter in GitAttributesCommand constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/GitAttributesCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index f1aa783..c72b1bb 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -61,7 +61,7 @@ final class GitAttributesCommand extends AbstractCommand * @param ReaderInterface $reader the reader component * @param WriterInterface $writer the writer component * @param Filesystem $filesystem the filesystem component - * @param ComposerJson $composerJson + * @param ComposerJson $composerJson the composer.json accessor */ public function __construct( private readonly CandidateProviderInterface $candidateProvider, From a79ede17422278993ce97ab5be3d464fd5138563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 17:58:32 -0300 Subject: [PATCH 08/14] refactor: improve documentation for logger parameter in SkillsSynchronizer constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Agent/Skills/SkillsSynchronizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Agent/Skills/SkillsSynchronizer.php b/src/Agent/Skills/SkillsSynchronizer.php index 871f5bd..3dd270b 100644 --- a/src/Agent/Skills/SkillsSynchronizer.php +++ b/src/Agent/Skills/SkillsSynchronizer.php @@ -38,7 +38,7 @@ final class SkillsSynchronizer implements LoggerAwareInterface * * @param Filesystem $filesystem Filesystem instance for file operations * @param Finder $finder Finder instance for locating skill directories in the package - * @param LoggerInterface $logger + * @param LoggerInterface $logger Logger for recording synchronization actions and decisions */ public function __construct( private readonly Filesystem $filesystem, From 75dda8e528f15b68ca60c06be5412e487366a6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:00:19 -0300 Subject: [PATCH 09/14] refactor: enhance constructor documentation for PhpDocCommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/PhpDocCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 9d8b49e..d9b9974 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -52,8 +52,10 @@ final class PhpDocCommand extends AbstractCommand public const string CONFIG = '.php-cs-fixer.dist.php'; /** - * @param ComposerJson $composerJson - * @param Filesystem $filesystem + * Creates a new PhpDocCommand instance. + * + * @param ComposerJson $composerJson the composer.json accessor + * @param Filesystem $filesystem the filesystem component */ public function __construct( private readonly ComposerJson $composerJson, From 026359cd8fcc646a11e7273b468f63fb61b7c98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:01:18 -0300 Subject: [PATCH 10/14] refactor: format AsCommand attributes in RefactorCommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/RefactorCommand.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index 31236a5..16e34c8 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -28,9 +28,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', -], help: 'This command runs Rector to refactor your code.')] +#[AsCommand( + name: 'refactor', + description: 'Runs Rector for code refactoring.', + aliases: ['rector'], + help: 'This command runs Rector to refactor your code.' +)] final class RefactorCommand extends AbstractCommand { /** From 08f53659a44a1b3f5764e4e9c645dde0ab52fc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:02:38 -0300 Subject: [PATCH 11/14] refactor: improve constructor documentation for WikiCommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/Console/Command/WikiCommand.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 2f63f2a..0bb7a0a 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -39,8 +39,10 @@ final class WikiCommand extends AbstractCommand { /** - * @param ComposerJson $composerJson - * @param Filesystem $filesystem + * Creates a new WikiCommand instance. + * + * @param ComposerJson $composerJson the composer.json accessor + * @param Filesystem $filesystem the filesystem component */ public function __construct( private readonly ComposerJson $composerJson, From d3633d4ab213626f8cb48840b5818b7a13c480f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:06:54 -0300 Subject: [PATCH 12/14] refactor: improve documentation for DevToolsCommandLoader class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- .../CommandLoader/DevToolsCommandLoader.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Console/CommandLoader/DevToolsCommandLoader.php b/src/Console/CommandLoader/DevToolsCommandLoader.php index b1fa506..8b26f61 100644 --- a/src/Console/CommandLoader/DevToolsCommandLoader.php +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -25,6 +25,21 @@ use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Finder\Finder; +/** + * Responsible for dynamically discovering and loading Symfony Console commands + * within the DevTools context. This class extends the ContainerCommandLoader + * and integrates with a PSR-11 compatible container to lazily instantiate commands. + * + * The implementation MUST scan a predefined directory for PHP classes representing + * console commands and SHALL only register classes that: + * - Are instantiable + * - Extend the Symfony\Component\Console\Command\Command base class + * - Declare the Symfony\Component\Console\Attribute\AsCommand attribute + * + * The command name MUST be extracted from the AsCommand attribute metadata and + * used as the key in the command map. Classes that do not meet these criteria + * MUST NOT be included in the command map. + */ final class DevToolsCommandLoader extends ContainerCommandLoader { /** From 0de8b19a0f0b44d539191bf0bd41bfd4fe7b2c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:08:04 -0300 Subject: [PATCH 13/14] refactor: improve constructor documentation for PlaceholderResolver class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/License/PlaceholderResolver.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/License/PlaceholderResolver.php b/src/License/PlaceholderResolver.php index a5b7435..4c3b3a6 100644 --- a/src/License/PlaceholderResolver.php +++ b/src/License/PlaceholderResolver.php @@ -32,7 +32,9 @@ final readonly class PlaceholderResolver implements PlaceholderResolverInterface { /** - * @param ClockInterface $clock + * Initializes the resolver with a clock for obtaining the current year if needed. + * + * @param ClockInterface $clock Clock instance for obtaining the current year if needed */ public function __construct( private ClockInterface $clock, From f1c13a3198f027fb706bc7f9aa5905c3bfa5ca32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 13 Apr 2026 18:10:13 -0300 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20melhorar=20a=20documenta?= =?UTF-8?q?=C3=A7=C3=A3o=20do=20construtor=20da=20classe=20Reader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felipe Sayão Lobato Abreu --- src/License/Reader.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/License/Reader.php b/src/License/Reader.php index 36f43cf..5a7e43a 100644 --- a/src/License/Reader.php +++ b/src/License/Reader.php @@ -20,7 +20,6 @@ use FastForward\DevTools\Composer\Json\ComposerJson; use Psr\Clock\ClockInterface; -use Safe\Exceptions\JsonException; /** * Reads composer.json and exposes metadata for license generation. @@ -35,10 +34,8 @@ * Creates a new Reader instance. * * @param ComposerJson $source The source file to read from, typically composer.json - * @param ClockInterface $clock - * @param ComposerJson $composerJson - * - * @throws JsonException if the JSON content is invalid + * @param ClockInterface $clock Clock instance for obtaining the current year if needed + * @param ComposerJson $composerJson the composer.json accessor for extracting metadata */ public function __construct( private ClockInterface $clock,