diff --git a/bin/dev-tools.php b/bin/dev-tools.php index b6d1646..cabafdf 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -18,6 +18,7 @@ namespace FastForward\DevTools; +use FastForward\DevTools\Console\DevTools; use Symfony\Component\Console\Input\ArgvInput; $projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; @@ -25,5 +26,4 @@ require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; -$application = new DevTools(); -$application->run(new ArgvInput([...$argv, '--no-plugins'])); +DevTools::create()->run(new ArgvInput([...$argv, '--no-plugins'])); diff --git a/composer.json b/composer.json index b18f188..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", @@ -36,11 +37,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/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..3dd270b 100644 --- a/src/Agent/Skills/SkillsSynchronizer.php +++ b/src/Agent/Skills/SkillsSynchronizer.php @@ -19,8 +19,7 @@ namespace FastForward\DevTools\Agent\Skills; use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; -use Psr\Log\NullLogger; +use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Filesystem\Path; @@ -34,21 +33,25 @@ */ 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 + * @param LoggerInterface $logger Logger for recording synchronization actions and decisions */ 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..737475e 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -18,54 +18,24 @@ 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\Command\BaseCommand; +use Composer\Plugin\Capability\CommandProvider; +use FastForward\DevTools\Console\DevTools; /** * 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 array_values(array_filter( + DevTools::create()->all(), + static fn(object $command): bool => $command instanceof BaseCommand, + )); } } diff --git a/src/Composer/Json/ComposerJson.php b/src/Composer/Json/ComposerJson.php new file mode 100644 index 0000000..18200e3 --- /dev/null +++ b/src/Composer/Json/ComposerJson.php @@ -0,0 +1,171 @@ + + * @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; +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; + + /** + * 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) + { + parent::__construct($path ?? Factory::getComposerFile()); + $this->data = $this->read(); + } + + /** + * 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 + { + return $this->data['name'] ?? ''; + } + + /** + * 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 + { + return $this->data['description'] ?? ''; + } + + /** + * 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 + { + $license = $this->data['license'] ?? []; + + if (\is_string($license)) { + return $license; + } + + if (\is_array($license) && 1 === \count($license)) { + return $license[0]; + } + + return null; + } + + /** + * 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 + { + return $this->data['authors'] ?? []; + } + + /** + * 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 + { + return $this->data['extra'] ?? []; + } + + /** + * 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 the autoload configuration for the requested type, + * or an empty array when unavailable + */ + 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 74% rename from src/Command/AbstractCommand.php rename to src/Console/Command/AbstractCommand.php index 643d5d5..6afd653 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,14 @@ */ 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(); } @@ -168,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)); } /** @@ -186,53 +178,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 58% rename from src/Command/CopyLicenseCommand.php rename to src/Console/Command/CopyLicenseCommand.php index b09f6e4..a2894b6 100644 --- a/src/Command/CopyLicenseCommand.php +++ b/src/Console/Command/CopyLicenseCommand.php @@ -16,16 +16,10 @@ * @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; 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; use Symfony\Component\Filesystem\Filesystem; @@ -36,50 +30,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 +70,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..7e5186c 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.', + aliases: ['deps'], + help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.' +)] 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 87% rename from src/Command/DocsCommand.php rename to src/Console/Command/DocsCommand.php index 6d8be36..603529b 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,24 @@ * 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 { + /** + * @param ComposerJson $composerJson + * @param Filesystem $filesystem + */ + public function __construct( + private readonly ComposerJson $composerJson, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the command instance. * @@ -47,9 +66,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 +149,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 +164,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..c72b1bb 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,29 @@ 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 + * @param ComposerJson $composerJson the composer.json accessor */ 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 +131,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..e4f3ee1 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 87% rename from src/Command/PhpDocCommand.php rename to src/Console/Command/PhpDocCommand.php index de5a65e..d9b9974 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,19 @@ final class PhpDocCommand extends AbstractCommand */ public const string CONFIG = '.php-cs-fixer.dist.php'; + /** + * 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, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the PHPDoc command. * @@ -53,9 +74,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 +188,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..16e34c8 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.', + aliases: ['rector'], + help: 'This command runs Rector to refactor your code.' +)] 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..91bc687 100644 --- a/src/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -16,12 +16,13 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Command; +namespace FastForward\DevTools\Console\Command; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; +use FastForward\DevTools\Composer\Json\ComposerJson; 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 +36,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 +49,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 +72,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 +233,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 75% rename from src/Command/WikiCommand.php rename to src/Console/Command/WikiCommand.php index 406afac..0bb7a0a 100644 --- a/src/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -16,19 +16,41 @@ * @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 { + /** + * 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, + Filesystem $filesystem + ) { + return parent::__construct($filesystem); + } + /** * Configures the command instance. * @@ -40,9 +62,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 +91,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..8b26f61 --- /dev/null +++ b/src/Console/CommandLoader/DevToolsCommandLoader.php @@ -0,0 +1,101 @@ + + * @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; + +/** + * 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 +{ + /** + * 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 + */ + public function __construct(Finder $finder, ContainerInterface $container) + { + parent::__construct($container, $this->getCommandMap($finder)); + } + + /** + * Builds a command map by scanning the Command directory for classes that are instantiable and have the AsCommand attribute. + * + * @param Finder $finder + * + * @return array + */ + private function getCommandMap(Finder $finder): array + { + $commandMap = []; + + $commandsDirectory = $finder + ->files() + ->in(__DIR__ . '/../Command') + ->name('*.php'); + + $namespace = substr(__NAMESPACE__, 0, strrpos(__NAMESPACE__, '\\')) . '\\Command\\'; + + foreach ($commandsDirectory as $file) { + $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 (null === $attribute) { + continue; + } + + $arguments = $attribute->getArguments(); + $commandMap[$arguments['name']] = $class; + } + + return $commandMap; + } +} diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php new file mode 100644 index 0000000..e3c7c3c --- /dev/null +++ b/src/Console/DevTools.php @@ -0,0 +1,87 @@ + + * @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 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; + +/** + * 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 +{ + /** + * @var ContainerInterface holds the static container instance for global access within the DevTools context + */ + private static ?ContainerInterface $container = null; + + /** + * 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); + } + + /** + * Create DevTools instance from container. + * + * @return DevTools + */ + public static function create(): self + { + if (! self::$container instanceof ContainerInterface) { + $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. + * + * The method SHOULD NOT add composer-specific commands to the list, + * as they are handled separately by composer when loaded as a plugin. + * + * @return array + */ + #[Override] + protected function getDefaultCommands(): array + { + $reflectionMethod = new ReflectionMethod(Application::class, __FUNCTION__); + + return $reflectionMethod->invoke($this); + } +} diff --git a/src/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..4c3b3a6 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; /** @@ -27,8 +29,17 @@ * {{ 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 { + /** + * 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, + ) {} + /** * Resolves placeholders in a license template with the provided metadata. * @@ -48,8 +59,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..5a7e43a 100644 --- a/src/License/Reader.php +++ b/src/License/Reader.php @@ -18,10 +18,8 @@ namespace FastForward\DevTools\License; -use Safe\Exceptions\JsonException; -use SplFileObject; - -use function Safe\json_decode; +use FastForward\DevTools\Composer\Json\ComposerJson; +use Psr\Clock\ClockInterface; /** * Reads composer.json and exposes metadata for license generation. @@ -32,35 +30,17 @@ */ final readonly class Reader implements ReaderInterface { - private array $data; - /** * Creates a new Reader instance. * - * @param SplFileObject $source The source file to read from, typically composer.json - * - * @throws JsonException if the JSON content is invalid + * @param ComposerJson $source The source file to read from, typically composer.json + * @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(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 ClockInterface $clock, + private ComposerJson $composerJson + ) {} /** * Retrieves the license identifier from composer.json. @@ -73,13 +53,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 +63,7 @@ public function getLicense(): ?string */ public function getPackageName(): string { - return $this->data['name'] ?? ''; + return $this->composerJson->getPackageName(); } /** @@ -102,7 +76,7 @@ public function getPackageName(): string */ public function getAuthors(): array { - $authors = $this->data['authors'] ?? []; + $authors = $this->composerJson->getAuthors(); if ([] === $authors) { return []; @@ -151,29 +125,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..1d67ebf --- /dev/null +++ b/src/Psr/Clock/SystemClock.php @@ -0,0 +1,40 @@ + + * @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 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 + { + return new DateTimeImmutable(); + } +} diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php new file mode 100644 index 0000000..d808651 --- /dev/null +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -0,0 +1,122 @@ + + * @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; +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 FastForward\DevTools\GitAttributes\Merger as GitAttributesMerger; +use FastForward\DevTools\GitAttributes\MergerInterface as GitAttributesMergerInterface; +use FastForward\DevTools\GitAttributes\Reader as GitAttributesReader; +use FastForward\DevTools\GitAttributes\ReaderInterface as GitAttributesReaderInterface; +use FastForward\DevTools\GitAttributes\Writer as GitAttributesWriter; +use FastForward\DevTools\GitAttributes\WriterInterface as GitAttributesWriterInterface; +use FastForward\DevTools\License\Reader as LicenseReader; +use FastForward\DevTools\License\ReaderInterface as LicenseReaderInterface; +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; + +/** + * 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 +{ + /** + * @return array + */ + public function getFactories(): array + { + 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), + 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), + ]; + } + + /** + * @return array + */ + public function getExtensions(): array + { + return []; + } +} 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 14527fc..0000000 --- a/tests/Command/AbstractCommandTest.php +++ /dev/null @@ -1,127 +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\Command\AbstractCommand; -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 string - */ - protected function getCommandClass(): string - { - return AbstractCommandStub::class; - } - - /** - * @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/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..d742e81 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; @@ -27,8 +28,10 @@ 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; + +use function Safe\getcwd; #[CoversClass(CopyLicenseCommand::class)] #[UsesClass(Reader::class)] @@ -41,11 +44,16 @@ 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 +80,27 @@ 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 +111,9 @@ 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..3016678 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,16 @@ final class DocsCommandTest extends AbstractCommandTestCase use ProphecyTrait; /** - * @return string + * @var ObjectProphecy */ - protected function getCommandClass(): string + private ObjectProphecy $composerJson; + + /** + * @return DocsCommand + */ + protected function getCommandClass(): DocsCommand { - return DocsCommand::class; + return new DocsCommand($this->composerJson->reveal(), $this->filesystem->reveal()); } /** @@ -64,6 +71,22 @@ protected function getCommandHelp(): string return 'This command generates API documentation using phpDocumentor.'; } + /** + * @return void + */ + 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(); + } + /** * @return void */ 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/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..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\Command\PhpDocCommand; -use FastForward\DevTools\Command\RefactorCommand; +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/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..b07e831 100644 --- a/tests/Command/ReportsCommandTest.php +++ b/tests/Command/ReportsCommandTest.php @@ -18,12 +18,11 @@ 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; 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/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..f95b1ae 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,11 @@ protected function getCommandHelp(): string 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 83f96d7..2ce0166 100644 --- a/tests/Command/WikiCommandTest.php +++ b/tests/Command/WikiCommandTest.php @@ -18,10 +18,12 @@ namespace FastForward\DevTools\Tests\Command; -use FastForward\DevTools\Command\WikiCommand; +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 81a7783..1d2e9f0 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -18,62 +18,29 @@ 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\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 Composer\Command\BaseCommand; 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\DevTools; 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; #[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)] final class DevToolsCommandProviderTest extends TestCase { + use ProphecyTrait; + + private ObjectProphecy $container; + + private ObjectProphecy $devTools; + private DevToolsCommandProvider $commandProvider; /** @@ -81,44 +48,50 @@ 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(); + + $property = new ReflectionProperty(DevTools::class, 'container'); + $property->setValue(null, $this->container->reveal()); } /** * @return void */ #[Test] - public function getCommandsWillReturnAllSupportedCommandsInExpectedOrder(): void + public function getCommandsWillReturnEmptyArrayWhenNoCommandsAreRegistered(): 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(), - ); + $commands = $this->commandProvider->getCommands(); + + self::assertIsArray($commands); + self::assertEmpty($commands); } /** * @return void */ #[Test] - public function getCommandsWillReturnOnlyAbstractCommandImplementations(): void + public function getCommandsWillReturnRegisteredBaseCommands(): void { - foreach ($this->commandProvider->getCommands() as $command) { - self::assertInstanceOf(AbstractCommand::class, $command); - } + $composerCommand = $this->prophesize(BaseCommand::class)->reveal(); + $symfonyCommand = $this->prophesize(Command::class)->reveal(); + + $this->devTools->all() + ->willReturn([$composerCommand, $symfonyCommand])->shouldBeCalledOnce(); + + $commands = $this->commandProvider->getCommands(); + + self::assertIsArray($commands); + self::assertCount(1, $commands); + self::assertSame($composerCommand, $commands[0]); } } 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/Console/DevToolsTest.php similarity index 60% rename from tests/DevToolsTest.php rename to tests/Console/DevToolsTest.php index 626b5a0..ac20b32 100644 --- a/tests/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -16,10 +16,9 @@ * @see https://datatracker.ietf.org/doc/html/rfc2119 */ -namespace FastForward\DevTools\Tests; +namespace FastForward\DevTools\Tests\Console; -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/ServiceProvider/DevToolsServiceProviderTest.php b/tests/ServiceProvider/DevToolsServiceProviderTest.php new file mode 100644 index 0000000..d1ac7af --- /dev/null +++ b/tests/ServiceProvider/DevToolsServiceProviderTest.php @@ -0,0 +1,69 @@ + + * @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; + +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; + + /** + * @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 + { + $factories = $this->provider->getFactories(); + + self::assertIsArray($factories); + self::assertNotEmpty($factories); + } +}