Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,44 @@
use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer;
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder;

use function Safe\getcwd;

/**
* Base ECS configuration for dev-tools.
*
* This configuration can be extended in consumer projects using one of the following patterns:
*
* 1. Use directly (recommended):
* ```php
* return require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php';
* ```
*
* 2. Extend with custom rules:
* ```php
* $builder = require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php';
* return $builder->withRules([CustomRule::class]);
* ```
*
* 3. Using the factory (recommended for advanced use cases):
* ```php
* use FastForward\DevTools\Config\EcsConfigFactory;
*
* return EcsConfigFactory::create();
* ```
*
* @return ECSConfigBuilder the pre-configured ECS configuration builder
*/
$cwd = getcwd();

return ECSConfig::configure()
->withPaths([getcwd()])
->withPaths([$cwd])
->withSkip([
getcwd() . '/public',
getcwd() . '/resources',
getcwd() . '/vendor',
getcwd() . '/tmp',
$cwd . '/public',
$cwd . '/resources',
$cwd . '/vendor',
$cwd . '/tmp',
PhpdocToCommentFixer::class,
NoSuperfluousPhpdocTagsFixer::class,
NoEmptyPhpdocFixer::class,
Expand Down
33 changes: 32 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,50 @@
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/

use Rector\Configuration\PhpLevelSetResolver;
use Composer\InstalledVersions;
use Ergebnis\Rector\Rules\Faker\GeneratorPropertyFetchToMethodCallRector;
use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector;
use FastForward\DevTools\Rector\RemoveEmptyDocBlockRector;
use Rector\Config\RectorConfig;
use Rector\Configuration\PhpLevelSetResolver;
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessParamTagRector;
use Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector;
use Rector\Php\PhpVersionResolver\ComposerJsonPhpVersionResolver;
use Rector\Set\ValueObject\SetList;

use function Safe\getcwd;

/**
* Base Rector configuration for dev-tools.
*
* This configuration can be extended in consumer projects using one of the following patterns:
*
* 1. Use directly (recommended):
* ```php
* return require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php';
* ```
*
* 2. Extend with custom rules:
* ```php
* $configure = require __DIR__ . '/vendor/fast-forward/dev-tools/rector.php';
* return static function (RectorConfig $rectorConfig) use ($configure): void {
* $configure($rectorConfig);
* $rectorConfig->rules([CustomRule::class]);
* };
* ```
*
* 3. Using the factory:
* ```php
* use FastForward\DevTools\Config\RectorConfigFactory;
*
* return static function (RectorConfig $rectorConfig): void {
* RectorConfigFactory::configure($rectorConfig);
* // Add custom configuration
* };
* ```
*
* @return callable(RectorConfig): void the configuration closure that configures Rector
*/
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
SetList::DEAD_CODE,
Expand Down
38 changes: 37 additions & 1 deletion src/Command/TestsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ protected function configure(): void
shortcut: 'f',
mode: InputOption::VALUE_OPTIONAL,
description: 'Filter which tests to run based on a pattern.',
)
->addOption(
name: 'parallel',
shortcut: 'p',
mode: InputOption::VALUE_OPTIONAL,
description: 'Run tests in parallel using ParaTest. Optional: specify number of workers (e.g., --parallel or --parallel=4).',
default: null,
);
}

Expand All @@ -102,8 +109,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Running PHPUnit tests...</info>');

$parallel = $input->getOption('parallel');
$runner = $this->getTestRunner($parallel !== false ? $parallel : null);

$arguments = [
$this->getAbsolutePath('vendor/bin/phpunit'),
$runner,
'--configuration=' . parent::getConfigFile(self::CONFIG),
'--bootstrap=' . $this->resolvePath($input, 'bootstrap'),
'--display-deprecations',
Expand All @@ -112,6 +122,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'--display-skipped',
];

if ($parallel !== null) {
if (is_numeric($parallel)) {
$arguments[] = '--processes=' . (int) $parallel;
}
$arguments[] = '--parallel';
}

if (! $input->getOption('no-cache')) {
$arguments[] = '--cache-directory=' . $this->resolvePath($input, 'cache-dir');
}
Expand Down Expand Up @@ -156,4 +173,23 @@ private function resolvePath(InputInterface $input, string $option): string
{
return $this->getAbsolutePath($input->getOption($option));
}

/**
* Determines the test runner to use based on parallel execution flag.
*
* The method MUST return the appropriate test runner binary path.
* If parallel is enabled, it SHALL return paratest; otherwise, it SHALL return phpunit.
*
* @param string|null $parallel the parallel option value
*
* @return string the path to the test runner binary
*/
private function getTestRunner(?string $parallel): string
{
if ($parallel !== null && $this->filesystem->exists($this->getAbsolutePath('vendor/bin/paratest'))) {
return $this->getAbsolutePath('vendor/bin/paratest');
}

return $this->getAbsolutePath('vendor/bin/phpunit');
}
}
146 changes: 146 additions & 0 deletions src/Config/EcsConfigFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/**
* This file is part of fast-forward/dev-tools.
*
* This source file is subject to the license bundled
* with this source code in the file LICENSE.
*
* @copyright Copyright (c) 2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @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\Config;

use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer;
use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer;
use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer;
use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocAddMissingParamAnnotationFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoEmptyReturnFixer;
use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer;
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder;

use function Safe\getcwd;

/**
* Factory for creating and extending the ECS (EasyCodingStandard) configuration.
*
* This factory allows consumers to build on top of the base configuration provided
* by the dev-tools package instead of replacing it entirely. Consumers can extend
* the configuration with custom rules, paths, skips, and other options while
* preserving the ability to receive upstream updates automatically.
*
* Usage examples:
*
* 1. Using the factory method (recommended):
* ```php
* use FastForward\DevTools\Config\EcsConfigFactory;
*
* return EcsConfigFactory::create();
* ```
*
* 2. Using the require pattern:
* ```php
* return require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php';
* ```
*
* 3. Extending the base configuration:
* ```php
* $builder = require __DIR__ . '/vendor/fast-forward/dev-tools/ecs.php';
* return $builder->withRules([CustomRule::class]);
* ```
*/
final class EcsConfigFactory
{
/**
* Creates and returns a pre-configured ECSConfigBuilder with the base dev-tools rules.
*
* This method returns the base configuration builder that consumers can further
* customize with additional rules, paths, skips, or configured rules.
*
* @return ECSConfigBuilder the pre-configured ECS configuration builder
*/
public static function create(): ECSConfigBuilder
{
$cwd = getcwd();

return ECSConfig::configure()
->withPaths([$cwd])
->withSkip([
$cwd . '/public',
$cwd . '/resources',
$cwd . '/vendor',
$cwd . '/tmp',
PhpdocToCommentFixer::class,
NoSuperfluousPhpdocTagsFixer::class,
NoEmptyPhpdocFixer::class,
PhpdocNoEmptyReturnFixer::class,
GlobalNamespaceImportFixer::class,
GeneralPhpdocAnnotationRemoveFixer::class,
])
->withRootFiles()
->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true)
->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true)
->withConfiguredRule(PhpdocAlignFixer::class, [
'align' => 'left',
])
->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [
'call_type' => 'self',
])
->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [
'only_untyped' => false,
]);
}

/**
* Loads and returns the base ECS configuration from the dev-tools package.
*
* This method can be used to require the base configuration file directly,
* providing full control over the configuration object.
*
* @param string|null $workingDirectory the working directory to use (defaults to cwd)
*
* @return ECSConfigBuilder the pre-configured ECS configuration builder
*/
public static function loadBaseConfiguration(?string $workingDirectory = null): ECSConfigBuilder
{
$cwd = $workingDirectory ?? getcwd();

return ECSConfig::configure()
->withPaths([$cwd])
->withSkip([
$cwd . '/public',
$cwd . '/resources',
$cwd . '/vendor',
$cwd . '/tmp',
PhpdocToCommentFixer::class,
NoSuperfluousPhpdocTagsFixer::class,
NoEmptyPhpdocFixer::class,
PhpdocNoEmptyReturnFixer::class,
GlobalNamespaceImportFixer::class,
GeneralPhpdocAnnotationRemoveFixer::class,
])
->withRootFiles()
->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true)
->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true)
->withConfiguredRule(PhpdocAlignFixer::class, [
'align' => 'left',
])
->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [
'call_type' => 'self',
])
->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [
'only_untyped' => false,
]);
}
}
Loading
Loading