Skip to content

Commit ba2b6c6

Browse files
committed
feat: make ecs.php extensible with EcsConfigFactory and CLI-level support
- Add EcsConfigFactory class for creating extensible ECS configurations - Implement factory pattern with withRules() and withConfiguredRule() methods - Add --extra-config option to CodeStyleCommand for layered configuration - Update ecs.php to use factory pattern instead of inline configuration - Add tests for EcsConfigFactory and CodeStyleCommand with extra-config option Implements: #6
1 parent 39d3384 commit ba2b6c6

5 files changed

Lines changed: 516 additions & 39 deletions

File tree

ecs.php

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,6 @@
1616
* @see https://datatracker.ietf.org/doc/html/rfc2119
1717
*/
1818

19-
use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer;
20-
use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer;
21-
use PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer;
22-
use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer;
23-
use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer;
24-
use PhpCsFixer\Fixer\Phpdoc\PhpdocAddMissingParamAnnotationFixer;
25-
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoEmptyReturnFixer;
26-
use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer;
27-
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer;
28-
use Symplify\EasyCodingStandard\Config\ECSConfig;
19+
use FastForward\DevTools\Config\EcsConfigFactory;
2920

30-
use function Safe\getcwd;
31-
32-
return ECSConfig::configure()
33-
->withPaths([getcwd()])
34-
->withSkip([
35-
getcwd() . '/public',
36-
getcwd() . '/resources',
37-
getcwd() . '/vendor',
38-
getcwd() . '/tmp',
39-
PhpdocToCommentFixer::class,
40-
NoSuperfluousPhpdocTagsFixer::class,
41-
NoEmptyPhpdocFixer::class,
42-
PhpdocNoEmptyReturnFixer::class,
43-
GlobalNamespaceImportFixer::class,
44-
GeneralPhpdocAnnotationRemoveFixer::class,
45-
])
46-
->withRootFiles()
47-
->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true)
48-
->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true)
49-
->withConfiguredRule(PhpdocAlignFixer::class, [
50-
'align' => 'left',
51-
])
52-
->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [
53-
'call_type' => 'self',
54-
])
55-
->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [
56-
'only_untyped' => false,
57-
]);
21+
return EcsConfigFactory::createWithDefaults();

src/Command/CodeStyleCommand.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ final class CodeStyleCommand extends AbstractCommand
3434
*/
3535
public const string CONFIG = 'ecs.php';
3636

37+
/**
38+
* @var string the extra configuration option name
39+
*/
40+
public const string EXTRA_CONFIG_OPTION = 'extra-config';
41+
3742
/**
3843
* Configures the current command.
3944
*
@@ -53,6 +58,12 @@ protected function configure(): void
5358
shortcut: 'f',
5459
mode: InputOption::VALUE_NONE,
5560
description: 'Automatically fix code style issues.'
61+
)
62+
->addOption(
63+
name: self::EXTRA_CONFIG_OPTION,
64+
shortcut: null,
65+
mode: InputOption::VALUE_REQUIRED,
66+
description: 'Additional ECS config file to merge with the default configuration.'
5667
);
5768
}
5869

@@ -79,12 +90,35 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7990

8091
parent::runProcess($command, $output);
8192

93+
$configFile = $this->resolveConfigFile($input);
94+
8295
$command = new Process([
8396
$this->getAbsolutePath('vendor/bin/ecs'),
84-
'--config=' . parent::getConfigFile(self::CONFIG),
97+
'--config=' . $configFile,
8598
$input->getOption('fix') ? '--fix' : '--clear-cache',
8699
]);
87100

88101
return parent::runProcess($command, $output);
89102
}
103+
104+
/**
105+
* Resolves the ECS configuration file path.
106+
*
107+
* This method determines the appropriate configuration file, supporting
108+
* layered configuration through the `--extra-config` option.
109+
*
110+
* @param InputInterface $input the input interface to retrieve options
111+
*
112+
* @return string the resolved absolute path to the configuration file
113+
*/
114+
private function resolveConfigFile(InputInterface $input): string
115+
{
116+
$extraConfig = $input->getOption(self::EXTRA_CONFIG_OPTION);
117+
118+
if (null !== $extraConfig && '' !== $extraConfig) {
119+
return $this->getAbsolutePath($extraConfig);
120+
}
121+
122+
return parent::getConfigFile(self::CONFIG);
123+
}
90124
}

src/Config/EcsConfigFactory.php

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of fast-forward/dev-tools.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @copyright Copyright (c) 2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
12+
* @license https://opensource.org/licenses/MIT MIT License
13+
*
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward
16+
* @see https://datatracker.ietf.org/doc/html/rfc2119
17+
*/
18+
19+
namespace FastForward\DevTools\Config;
20+
21+
use PhpCsFixer\Fixer\Import\GlobalNamespaceImportFixer;
22+
use PhpCsFixer\Fixer\Phpdoc\GeneralPhpdocAnnotationRemoveFixer;
23+
use PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer;
24+
use PhpCsFixer\Fixer\Phpdoc\NoEmptyPhpdocFixer;
25+
use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer;
26+
use PhpCsFixer\Fixer\Phpdoc\PhpdocAddMissingParamAnnotationFixer;
27+
use PhpCsFixer\Fixer\Phpdoc\PhpdocNoEmptyReturnFixer;
28+
use PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer;
29+
use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer;
30+
use Symplify\EasyCodingStandard\Config\ECSConfig;
31+
use Symplify\EasyCodingStandard\Configuration\ECSConfigBuilder;
32+
33+
use function Safe\getcwd;
34+
35+
/**
36+
* Provides a factory for creating extensible ECS configurations.
37+
*
38+
* This class enables consumers to build upon the default configuration
39+
* without copying the entire file, reducing maintenance overhead and
40+
* ensuring upstream updates are automatically propagated.
41+
*/
42+
final class EcsConfigFactory
43+
{
44+
private readonly ECSConfigBuilder $builder;
45+
46+
private array $skip = [];
47+
48+
private array $additionalRules = [];
49+
50+
private array $additionalRuleConfigurations = [];
51+
52+
private function __construct()
53+
{
54+
$this->builder = ECSConfig::configure();
55+
}
56+
57+
/**
58+
* Creates a new factory instance with default configuration.
59+
*/
60+
public static function create(): self
61+
{
62+
return new self();
63+
}
64+
65+
/**
66+
* Creates a new factory instance with the default configuration pre-loaded.
67+
*
68+
* This method provides a pre-configured ECSConfig that can be further
69+
* customized by consumers. It is equivalent to requiring the base ecs.php.
70+
*/
71+
public static function createWithDefaults(): ECSConfigBuilder
72+
{
73+
$cwd = getcwd();
74+
75+
return ECSConfig::configure()
76+
->withPaths([$cwd])
77+
->withSkip([
78+
$cwd . '/public',
79+
$cwd . '/resources',
80+
$cwd . '/vendor',
81+
$cwd . '/tmp',
82+
PhpdocToCommentFixer::class,
83+
NoSuperfluousPhpdocTagsFixer::class,
84+
NoEmptyPhpdocFixer::class,
85+
PhpdocNoEmptyReturnFixer::class,
86+
GlobalNamespaceImportFixer::class,
87+
GeneralPhpdocAnnotationRemoveFixer::class,
88+
])
89+
->withRootFiles()
90+
->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true)
91+
->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true)
92+
->withConfiguredRule(PhpdocAlignFixer::class, [
93+
'align' => 'left',
94+
])
95+
->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [
96+
'call_type' => 'self',
97+
])
98+
->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [
99+
'only_untyped' => false,
100+
]);
101+
}
102+
103+
/**
104+
* Adds paths to be processed by ECS.
105+
*
106+
* @param array<int, string> $paths the paths to include
107+
*/
108+
public function withPaths(array $paths): self
109+
{
110+
$this->builder->withPaths($paths);
111+
112+
return $this;
113+
}
114+
115+
/**
116+
* Adds paths or classes to skip during ECS processing.
117+
*
118+
* @param array<int, string> $skip the paths or classes to skip
119+
*/
120+
public function withSkip(array $skip): self
121+
{
122+
$this->skip = array_merge($this->skip, $skip);
123+
124+
return $this;
125+
}
126+
127+
/**
128+
* Includes root files in the ECS configuration.
129+
*/
130+
public function withRootFiles(): self
131+
{
132+
$this->builder->withRootFiles();
133+
134+
return $this;
135+
}
136+
137+
/**
138+
* Includes PHP-CS-Fixer rule sets.
139+
*
140+
* @param bool $symfony include Symfony set
141+
* @param bool $symfonyRisky include SymfonyRisky set
142+
* @param bool $auto include auto set
143+
* @param bool $autoRisky include autoRisky set
144+
*/
145+
public function withPhpCsFixerSets(
146+
bool $symfony = false,
147+
bool $symfonyRisky = false,
148+
bool $auto = false,
149+
bool $autoRisky = false
150+
): self {
151+
$this->builder->withPhpCsFixerSets(
152+
symfony: $symfony,
153+
symfonyRisky: $symfonyRisky,
154+
auto: $auto,
155+
autoRisky: $autoRisky
156+
);
157+
158+
return $this;
159+
}
160+
161+
/**
162+
* Includes prepared rule sets.
163+
*
164+
* @param bool $psr12 include PSR-12 set
165+
* @param bool $common include common set
166+
* @param bool $symplify include Symplify set
167+
* @param bool $strict include strict set
168+
* @param bool $cleanCode include cleanCode set
169+
*/
170+
public function withPreparedSets(
171+
bool $psr12 = false,
172+
bool $common = false,
173+
bool $symplify = false,
174+
bool $strict = false,
175+
bool $cleanCode = false
176+
): self {
177+
$this->builder->withPreparedSets(
178+
psr12: $psr12,
179+
common: $common,
180+
symplify: $symplify,
181+
strict: $strict,
182+
cleanCode: $cleanCode
183+
);
184+
185+
return $this;
186+
}
187+
188+
/**
189+
* Adds additional rules to the configuration.
190+
*
191+
* @param array<int, class-string> $rules the rule classes to add
192+
*/
193+
public function withRules(array $rules): self
194+
{
195+
$this->additionalRules = array_merge($this->additionalRules, $rules);
196+
197+
return $this;
198+
}
199+
200+
/**
201+
* Configures a specific rule with options.
202+
*
203+
* @param class-string $ruleClass the rule class to configure
204+
* @param array<mixed> $configuration the configuration options
205+
*/
206+
public function withConfiguredRule(string $ruleClass, array $configuration): self
207+
{
208+
$this->additionalRuleConfigurations[$ruleClass] = $configuration;
209+
210+
return $this;
211+
}
212+
213+
/**
214+
* Builds and returns the configured ECSConfig instance.
215+
*/
216+
public function build(): ECSConfigBuilder
217+
{
218+
$cwd = getcwd();
219+
220+
$this->builder->withSkip(array_merge([
221+
$cwd . '/public',
222+
$cwd . '/resources',
223+
$cwd . '/vendor',
224+
$cwd . '/tmp',
225+
PhpdocToCommentFixer::class,
226+
NoSuperfluousPhpdocTagsFixer::class,
227+
NoEmptyPhpdocFixer::class,
228+
PhpdocNoEmptyReturnFixer::class,
229+
GlobalNamespaceImportFixer::class,
230+
GeneralPhpdocAnnotationRemoveFixer::class,
231+
], $this->skip));
232+
233+
$this->builder->withPhpCsFixerSets(symfony: true, symfonyRisky: true, auto: true, autoRisky: true);
234+
$this->builder->withPreparedSets(psr12: true, common: true, symplify: true, strict: true, cleanCode: true);
235+
$this->builder->withConfiguredRule(PhpdocAlignFixer::class, [
236+
'align' => 'left',
237+
]);
238+
$this->builder->withConfiguredRule(PhpUnitTestCaseStaticMethodCallsFixer::class, [
239+
'call_type' => 'self',
240+
]);
241+
$this->builder->withConfiguredRule(PhpdocAddMissingParamAnnotationFixer::class, [
242+
'only_untyped' => false,
243+
]);
244+
245+
foreach ($this->additionalRules as $rule) {
246+
$this->builder->withRules([$rule]);
247+
}
248+
249+
foreach ($this->additionalRuleConfigurations as $ruleClass => $configuration) {
250+
$this->builder->withConfiguredRule($ruleClass, $configuration);
251+
}
252+
253+
return $this->builder;
254+
}
255+
}

0 commit comments

Comments
 (0)