Skip to content
57 changes: 45 additions & 12 deletions src/Console/Command/CodeStyleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

namespace FastForward\DevTools\Console\Command;

use Composer\Command\BaseCommand;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Symfony\Component\Config\FileLocatorInterface;
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\Process\Process;

/**
* Represents the command responsible for checking and fixing code style issues.
Expand All @@ -33,13 +36,33 @@
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
final class CodeStyleCommand extends BaseCommand
{
/**
* @var string the default configuration file used for EasyCodingStandard
*/
public const string CONFIG = 'ecs.php';

/**
* Constructs a new command instance responsible for orchestrating code style checks.
*
* The provided collaborators SHALL be used to locate the ECS configuration,
* build process definitions, and execute the resulting process queue. These
* dependencies MUST be valid service instances capable of supporting the
* command lifecycle expected by this class.
*
* @param FileLocatorInterface $fileLocator locates the configuration file required by EasyCodingStandard
* @param ProcessBuilderInterface $processBuilder builds the process instances used to execute Composer and ECS commands
* @param ProcessQueueInterface $processQueue queues and executes the generated processes in the required order
*/
public function __construct(
private readonly FileLocatorInterface $fileLocator,
private readonly ProcessBuilderInterface $processBuilder,
private readonly ProcessQueueInterface $processQueue,
) {
parent::__construct();
}

/**
* Configures the current command.
*
Expand Down Expand Up @@ -74,20 +97,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Running code style checks and fixes...</info>');

$command = new Process(['composer', 'update', '--lock', '--quiet']);
$composerUpdate = $this->processBuilder
->withArgument('--lock')
->withArgument('--quiet')
->build('composer update');

$composerNormalize = $this->processBuilder
->withArgument('--ansi')
->withArgument($input->getOption('fix') ? '--quiet' : '--dry-run')
->build('composer normalize');

parent::runProcess($command, $output);
$processBuilder = $this->processBuilder
->withArgument('--no-progress-bar')
->withArgument('--config', $this->fileLocator->locate(self::CONFIG));

$command = new Process(['composer', 'normalize', $input->getOption('fix') ? '--quiet' : '--dry-run']);
if ($input->getOption('fix')) {
$processBuilder = $processBuilder->withArgument('--fix');
}

parent::runProcess($command, $output);
$ecs = $processBuilder->build('vendor/bin/ecs');

$command = new Process([
$this->getAbsolutePath('vendor/bin/ecs'),
'--config=' . parent::getConfigFile(self::CONFIG),
$input->getOption('fix') ? '--fix' : '--clear-cache',
]);
$this->processQueue->add($composerUpdate);
$this->processQueue->add($composerNormalize);
$this->processQueue->add($ecs);

return parent::runProcess($command, $output);
return $this->processQueue->run($output);
}
}
47 changes: 33 additions & 14 deletions src/Console/Command/DependenciesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@

namespace FastForward\DevTools\Console\Command;

use Composer\Command\BaseCommand;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;

/**
* Orchestrates dependency analysis across the supported Composer analyzers.
Expand All @@ -34,8 +37,21 @@
aliases: ['deps'],
help: 'This command runs composer-dependency-analyser and composer-unused to report missing and unused Composer dependencies.'
)]
final class DependenciesCommand extends AbstractCommand
final class DependenciesCommand extends BaseCommand
{
/**
* @param ProcessBuilderInterface $processBuilder
* @param ProcessQueueInterface $processQueue
* @param FileLocatorInterface $fileLocator
*/
public function __construct(
private readonly ProcessBuilderInterface $processBuilder,
private readonly ProcessQueueInterface $processQueue,
private readonly FileLocatorInterface $fileLocator,
) {
return parent::__construct();
}

/**
* Executes the dependency analysis workflow.
*
Expand All @@ -52,19 +68,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Running dependency analysis...</info>');

$composerJson = $this->getConfigFile('composer.json');
$composerJson = $this->fileLocator->locate('composer.json');

$composerUnused = $this->processBuilder
->withArgument($composerJson)
->withArgument('--no-progress')
->build('vendor/bin/composer-unused');

$composerDependencyAnalyser = $this->processBuilder
->withArgument('--composer-json', $composerJson)
->withArgument('--ignore-unused-deps')
->withArgument('--ignore-prod-only-in-dev-deps')
->build('vendor/bin/composer-dependency-analyser');

$results[] = $this->runProcess(
new Process(['vendor/bin/composer-unused', $composerJson, '--no-progress']),
$output
);
$results[] = $this->runProcess(new Process([
'vendor/bin/composer-dependency-analyser',
'--composer-json=' . $composerJson,
'--ignore-unused-deps',
'--ignore-prod-only-in-dev-deps',
]), $output);
$this->processQueue->add($composerUnused);
$this->processQueue->add($composerDependencyAnalyser);

return \in_array(self::FAILURE, $results, true) ? self::FAILURE : self::SUCCESS;
return $this->processQueue->run($output);
}
}
20 changes: 13 additions & 7 deletions src/Console/Command/GitIgnoreCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

namespace FastForward\DevTools\Console\Command;

use Composer\Command\BaseCommand;
use FastForward\DevTools\GitIgnore\MergerInterface;
use FastForward\DevTools\GitIgnore\ReaderInterface;
use FastForward\DevTools\GitIgnore\WriterInterface;
use Symfony\Component\Config\FileLocatorInterface;
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;

/**
* Provides functionality to merge and synchronize .gitignore files.
Expand All @@ -41,23 +42,28 @@
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
final class GitIgnoreCommand extends BaseCommand
{
/**
* @var string the default filename for .gitignore files
*/
public const string FILENAME = '.gitignore';

/**
* Creates a new GitIgnoreCommand instance.
*
* @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
* @param FilelocatorInterface $fileLocator the file locator
*/
public function __construct(
private readonly MergerInterface $merger,
private readonly ReaderInterface $reader,
private readonly WriterInterface $writer,
Filesystem $filesystem,
private readonly FileLocatorInterface $fileLocator,
) {
parent::__construct($filesystem);
parent::__construct();
}

/**
Expand All @@ -74,14 +80,14 @@ protected function configure(): void
shortcut: 's',
mode: InputOption::VALUE_OPTIONAL,
description: 'Path to the source .gitignore file (canonical)',
default: parent::getDevToolsFile('.gitignore'),
default: $this->fileLocator->locate(self::FILENAME, dirname(__DIR__, 3)),
)
->addOption(
name: 'target',
shortcut: 't',
mode: InputOption::VALUE_OPTIONAL,
description: 'Path to the target .gitignore file (project)',
default: parent::getConfigFile('.gitignore', true)
default: $this->fileLocator->locate(self::FILENAME)
);
}

Expand Down
49 changes: 40 additions & 9 deletions src/Console/Command/RefactorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

namespace FastForward\DevTools\Console\Command;

use Composer\Command\BaseCommand;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -34,13 +38,28 @@
aliases: ['rector'],
help: 'This command runs Rector to refactor your code.'
)]
final class RefactorCommand extends AbstractCommand
final class RefactorCommand extends BaseCommand
{
/**
* @var string the default Rector configuration file
*/
public const string CONFIG = 'rector.php';

/**
* Creates a new RefactorCommand instance.
*
* @param FileLocatorInterface $fileLocator the file locator
* @param ProcessBuilderInterface $processBuilder the process builder
* @param ProcessQueueInterface $processQueue the process queue
*/
public function __construct(
private readonly FileLocatorInterface $fileLocator,
private readonly ProcessBuilderInterface $processBuilder,
private readonly ProcessQueueInterface $processQueue,
) {
parent::__construct();
}

/**
* Configures the refactor command options and description.
*
Expand All @@ -57,6 +76,13 @@ protected function configure(): void
shortcut: 'f',
mode: InputOption::VALUE_NONE,
description: 'Automatically fix code refactoring issues.'
)
->addOption(
name: 'config',
shortcut: 'c',
mode: InputOption::VALUE_OPTIONAL,
description: 'The path to the Rector configuration file.',
default: self::CONFIG
);
}

Expand All @@ -75,14 +101,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Running Rector for code refactoring...</info>');

$command = new Process([
$this->getAbsolutePath('vendor/bin/rector'),
'process',
'--config',
parent::getConfigFile(self::CONFIG),
$input->getOption('fix') ? null : '--dry-run',
]);
$processBuilder = $this->processBuilder
->withArgument('process')
->withArgument('--config')
->withArgument($this->fileLocator->locate(self::CONFIG));

if (! $input->getOption('fix')) {
$processBuilder = $processBuilder->withArgument('--dry-run');
}

$this->processQueue->add(
$processBuilder->build('vendor/bin/rector')
);

return parent::runProcess($command, $output);
return $this->processQueue->run($output);
}
}
57 changes: 47 additions & 10 deletions src/Console/Command/ReportsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

namespace FastForward\DevTools\Console\Command;

use Composer\Command\BaseCommand;
use Composer\Console\Input\InputOption;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand All @@ -31,8 +35,39 @@
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
final class ReportsCommand extends BaseCommand
{
/**
* Initializes the command with required dependencies.
*
* @param ProcessBuilderInterface $processBuilder the builder instance used to construct execution processes
* @param ProcessQueueInterface $processQueue the execution queue mechanism for running sub-processes
*/
public function __construct(
private readonly ProcessBuilderInterface $processBuilder,
private readonly ProcessQueueInterface $processQueue,
) {
parent::__construct();
}

public function configure(): void
{
$this
->addOption(
name: 'target',
mode: InputOption::VALUE_OPTIONAL,
description: 'The target directory for the generated reports.',
default: 'public',
)
->addOption(
name: 'coverage',
shortcut: 'c',
mode: InputOption::VALUE_OPTIONAL,
description: 'The target directory for the generated test coverage report.',
default: 'public/coverage',
);
}

/**
* Executes the generation logic for diverse reports.
*
Expand All @@ -48,17 +83,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Generating frontpage for Fast Forward documentation...</info>');

$docsPath = $this->getAbsolutePath('public');
$coveragePath = $this->getAbsolutePath('public/coverage');

$results = [];
$docs = $this->processBuilder
->withArgument('--ansi')
->withArgument('--target', $input->getOption('target'))
->build('composer dev-tools docs');

$output->writeln('<info>Generating API documentation on path: ' . $docsPath . '</info>');
$results[] = $this->runCommand('docs --target=' . $docsPath, $output);
$coverage = $this->processBuilder
->withArgument('--ansi')
->withArgument('--coverage', $input->getOption('coverage'))
->build('composer dev-tools tests');

$output->writeln('<info>Generating test coverage report on path: ' . $coveragePath . '</info>');
$results[] = $this->runCommand('tests --coverage=' . $coveragePath, $output);
$this->processQueue->add(process: $docs, detached: true);
$this->processQueue->add(process: $coverage, detached: true);

return \in_array(self::FAILURE, $results, true) ? self::FAILURE : self::SUCCESS;
return $this->processQueue->run($output);
}
}
Loading
Loading