1919use ReflectionNamedType ;
2020use ReflectionParameter ;
2121use ReflectionUnionType ;
22+ use Respect \Dev \Differ \ConsoleDiffer ;
23+ use Respect \Dev \Differ \Item ;
2224use Respect \Validation \Mixins \AllBuilder ;
2325use Respect \Validation \Mixins \AllChain ;
2426use Respect \Validation \Mixins \Chain ;
4446use Symfony \Component \Console \Command \Command ;
4547use Symfony \Component \Console \Input \InputInterface ;
4648use Symfony \Component \Console \Output \OutputInterface ;
47- use Symfony \Component \Console \Style \SymfonyStyle ;
4849
49- use function array_filter ;
50+ use function array_keys ;
5051use function array_merge ;
52+ use function array_values ;
5153use function count ;
5254use function dirname ;
53- use function file_exists ;
55+ use function file_get_contents ;
5456use function file_put_contents ;
5557use function implode ;
5658use function in_array ;
5759use function is_object ;
5860use function ksort ;
5961use function lcfirst ;
62+ use function preg_match ;
6063use function preg_replace ;
61- use function shell_exec ;
6264use function sprintf ;
6365use function str_contains ;
6466use function str_starts_with ;
67+ use function trim ;
6568use function ucfirst ;
6669
70+ use const PHP_EOL ;
71+
6772#[AsCommand(
68- name: 'create :mixin ' ,
69- description: 'Generate mixin interfaces from validators ' ,
73+ name: 'lint :mixin ' ,
74+ description: 'Apply linters to the generated mixin interfaces ' ,
7075)]
71- final class CreateMixinCommand extends Command
76+ final class LintMixinCommand extends Command
7277{
7378 private const array NUMBER_RELATED_VALIDATORS = [
7479 'Between ' ,
@@ -109,19 +114,29 @@ final class CreateMixinCommand extends Command
109114 'Named ' ,
110115 ];
111116
112- protected function execute (InputInterface $ input , OutputInterface $ output ): int
113- {
114- $ io = new SymfonyStyle ($ input , $ output );
117+ public function __construct (
118+ private readonly ConsoleDiffer $ differ ,
119+ ) {
120+ parent ::__construct ();
121+ }
115122
116- $ io ->title ('Generating mixin interfaces ' );
123+ protected function configure (): void
124+ {
125+ $ this ->addOption (
126+ 'fix ' ,
127+ null ,
128+ null ,
129+ 'Automatically fix files with issues. ' ,
130+ );
131+ }
117132
133+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
134+ {
118135 // Scan validators directory
119136 $ srcDir = dirname (__DIR__ , 2 ) . '/src ' ;
120137 $ validatorsDir = $ srcDir . '/Validators ' ;
121138 $ validators = $ this ->scanValidators ($ validatorsDir );
122139
123- $ io ->text (sprintf ('Found %d validators ' , count ($ validators )));
124-
125140 // Define mixins
126141 $ mixins = [
127142 ['All ' , 'all ' , [], array_merge (['All ' ], self ::STRUCTURE_RELATED_VALIDATORS )],
@@ -133,23 +148,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
133148 ['NullOr ' , 'nullOr ' , [], ['NullOr ' , 'Blank ' , 'Undef ' , 'UndefOr ' , 'Templated ' , 'Named ' ]],
134149 ['Property ' , 'property ' , [], self ::STRUCTURE_RELATED_VALIDATORS ],
135150 ['UndefOr ' , 'undefOr ' , [], ['NullOr ' , 'Blank ' , 'Undef ' , 'UndefOr ' , 'Attributes ' , 'Templated ' , 'Named ' ]],
136- ['' , null , [], []],
151+ [null , null , [], []],
137152 ];
138153
139- $ io -> section ( ' Generating mixin interfaces ' ) ;
154+ $ updatableFiles = [] ;
140155
141156 foreach ($ mixins as [$ name , $ prefix , $ allowList , $ denyList ]) {
142- $ io ->text (sprintf ('Generating %sBuilder and %sChain ' , $ name ?: 'Base ' , $ name ?: 'Base ' ));
143-
144157 $ chainedNamespace = new PhpNamespace ('Respect \\Validation \\Mixins ' );
145- $ chainedNamespace ->addUse (Validator::class);
146158 $ chainedInterface = $ chainedNamespace ->addInterface ($ name . 'Chain ' );
147159
148160 $ staticNamespace = new PhpNamespace ('Respect \\Validation \\Mixins ' );
149- $ staticNamespace ->addUse (Validator::class);
150161 $ staticInterface = $ staticNamespace ->addInterface ($ name . 'Builder ' );
151162
152- if ($ name === '' ) {
163+ if ($ name === null ) {
153164 $ chainedInterface ->addExtend (Validator::class);
154165 $ chainedInterface ->addExtend (AllChain::class);
155166 $ chainedInterface ->addExtend (KeyChain::class);
@@ -160,7 +171,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
160171 $ chainedInterface ->addExtend (NullOrChain::class);
161172 $ chainedInterface ->addExtend (PropertyChain::class);
162173 $ chainedInterface ->addExtend (UndefOrChain::class);
163- $ chainedInterface ->addComment ('@mixin \\' . ValidatorBuilder::class);
174+ $ chainedInterface ->addComment ('@mixin ValidatorBuilder ' );
175+ $ chainedNamespace ->addUse (ValidatorBuilder::class);
164176
165177 $ staticInterface ->addExtend (AllBuilder::class);
166178 $ staticInterface ->addExtend (KeyBuilder::class);
@@ -175,6 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
175187
176188 foreach ($ validators as $ originalName => $ reflection ) {
177189 $ this ->addMethodToInterface (
190+ $ staticNamespace ,
178191 $ originalName ,
179192 $ staticInterface ,
180193 $ reflection ,
@@ -183,6 +196,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
183196 $ denyList ,
184197 );
185198 $ this ->addMethodToInterface (
199+ $ chainedNamespace ,
186200 $ originalName ,
187201 $ chainedInterface ,
188202 $ reflection ,
@@ -193,25 +207,42 @@ protected function execute(InputInterface $input, OutputInterface $output): int
193207 }
194208
195209 $ printer = new Printer ();
196- $ printer ->wrapLength = 115 ;
210+ $ printer ->wrapLength = 300 ;
197211
198- $ this ->overwriteFile ($ printer ->printNamespace ($ staticNamespace ), $ staticInterface ->getName ());
199- $ this ->overwriteFile ($ printer ->printNamespace ($ chainedNamespace ), $ chainedInterface ->getName ());
212+ foreach (
213+ [
214+ [$ staticNamespace , $ staticInterface ],
215+ [$ chainedNamespace , $ chainedInterface ],
216+ ] as [$ namespace , $ interface ]
217+ ) {
218+ $ filename = sprintf ('%s/Mixins/%s.php ' , $ srcDir , $ interface ->getName ());
219+ $ existingContent = file_get_contents ($ filename );
220+ $ formattedContent = $ this ->getFormattedContent ($ printer ->printNamespace ($ namespace ), $ existingContent );
221+ if ($ formattedContent === $ existingContent ) {
222+ continue ;
223+ }
224+
225+ $ updatableFiles [$ filename ] = $ formattedContent ;
226+ $ output ->writeln ($ this ->differ ->diff (
227+ new Item ($ filename , $ existingContent ),
228+ new Item ($ filename , $ formattedContent ),
229+ ));
230+ }
200231 }
201232
202- // Run code beautifier
203- $ io ->section ('Running code beautifier ' );
204- $ mixinsDir = $ srcDir . '/Mixins ' ;
205- $ phpcbfPath = dirname (__DIR__ , 2 ) . '/vendor/bin/phpcbf ' ;
206-
207- if (file_exists ($ phpcbfPath )) {
208- shell_exec ($ phpcbfPath . ' ' . $ mixinsDir );
209- $ io ->success ('Code beautified ' );
233+ if ($ updatableFiles === []) {
234+ $ output ->writeln ('<info>No changes needed.</info> ' );
210235 } else {
211- $ io -> warning ( ' phpcbf not found, skipping code beautification ' );
236+ $ output -> writeln ( sprintf ( ' <comment>Changes needed in %d files.</comment> ' , count ( $ updatableFiles )) );
212237 }
213238
214- $ io ->success ('Mixin interfaces generated successfully ' );
239+ if ($ updatableFiles !== [] && !$ input ->getOption ('fix ' )) {
240+ return Command::FAILURE ;
241+ }
242+
243+ foreach ($ updatableFiles as $ filename => $ content ) {
244+ file_put_contents ($ filename , $ content );
245+ }
215246
216247 return Command::SUCCESS ;
217248 }
@@ -246,6 +277,7 @@ private function scanValidators(string $directory): array
246277 * @param array<string> $denyList
247278 */
248279 private function addMethodToInterface (
280+ PhpNamespace $ namespace ,
249281 string $ originalName ,
250282 InterfaceType $ interfaceType ,
251283 ReflectionClass $ reflection ,
@@ -287,12 +319,15 @@ private function addMethodToInterface(
287319 }
288320
289321 foreach ($ reflectionConstructor ->getParameters () as $ reflectionParameter ) {
290- $ this ->addParameterToMethod ($ method , $ reflectionParameter );
322+ $ this ->addParameterToMethod ($ method , $ reflectionParameter, $ namespace );
291323 }
292324 }
293325
294- private function addParameterToMethod (Method $ method , ReflectionParameter $ reflectionParameter ): void
295- {
326+ private function addParameterToMethod (
327+ Method $ method ,
328+ ReflectionParameter $ reflectionParameter ,
329+ PhpNamespace $ namespace ,
330+ ): void {
296331 if ($ reflectionParameter ->isVariadic ()) {
297332 $ method ->setVariadic ();
298333 }
@@ -303,6 +338,11 @@ private function addParameterToMethod(Method $method, ReflectionParameter $refle
303338 if ($ type instanceof ReflectionUnionType) {
304339 foreach ($ type ->getTypes () as $ subType ) {
305340 $ types [] = $ subType ->getName ();
341+ if ($ subType ->isBuiltin ()) {
342+ continue ;
343+ }
344+
345+ $ namespace ->addUse ($ subType ->getName ());
306346 }
307347 } elseif ($ type instanceof ReflectionNamedType) {
308348 $ types [] = $ type ->getName ();
@@ -313,6 +353,10 @@ private function addParameterToMethod(Method $method, ReflectionParameter $refle
313353 ) {
314354 return ;
315355 }
356+
357+ if (!$ type ->isBuiltin ()) {
358+ $ namespace ->addUse ($ type ->getName ());
359+ }
316360 }
317361
318362 $ parameter = $ method ->addParameter ($ reflectionParameter ->getName ());
@@ -342,25 +386,27 @@ private function addParameterToMethod(Method $method, ReflectionParameter $refle
342386 $ parameter ->setNullable (false );
343387 }
344388
345- private function overwriteFile (string $ content , string $ basename ): void
389+ private function getFormattedContent (string $ content , string $ existingContent ): string
346390 {
347- $ srcDir = dirname (__DIR__ , 2 ) . '/src ' ;
348-
349- $ SPDX = ' * SPDX ' ;
391+ preg_match ('/^<\?php\s*\/\*[\s\S]*?\*\// ' , $ existingContent , $ matches );
392+ $ existingHeader = $ matches [0 ] ?? '' ;
393+
394+ $ replacements = [
395+ '/\n\n\t(public|\/\*\*)/m ' => PHP_EOL . ' $1 ' ,
396+ '/\t/m ' => ' ' ,
397+ '/\?([a-zA-Z]+) \$/ ' => '$1|null $ ' ,
398+ '/\/\*\*\n +\* (.+)\n +\*\//m ' => '/** $1 */ ' ,
399+ ];
350400
351- $ finalContent = implode ("\n\n" , array_filter ([
352- '<?php ' ,
353- '/* ' ,
354- $ SPDX . '-License-Identifier: MIT ' ,
355- $ SPDX . '-FileCopyrightText: (c) Respect Project Contributors ' ,
356- '/* ' ,
401+ return implode (PHP_EOL , [
402+ trim ($ existingHeader ) . PHP_EOL ,
357403 'declare(strict_types=1); ' ,
358- preg_replace ( ' /extends (.+, )+/ ' , ' extends ' . "\n" . ' \1 ' , $ content ) ,
359- ]));
360-
361- file_put_contents (
362- sprintf ( ' %s/Mixins/%s.php ' , $ srcDir , $ basename ) ,
363- $ finalContent ,
364- );
404+ '' ,
405+ preg_replace (
406+ array_keys ( $ replacements ),
407+ array_values ( $ replacements ),
408+ $ content ,
409+ ) ,
410+ ] );
365411 }
366412}
0 commit comments