From 90f15e98f1342eed2c536b0953fa225ecf71d4b6 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 9 Feb 2026 23:20:28 +0100 Subject: [PATCH 1/2] [TASK] Refactor command input handling and remove redundant validators The Decorators no longer instantiate Normalizers to avoid duplicate work. Specialized validators (Command, Controller, Event, etc.) have been removed in favor of calling ClassNameValidator directly. Since the Normalizer now ensures the correct suffixes are present before validation occurs, the suffix-specific checks are no longer required. --- .../Decorator/CommandClassNameDecorator.php | 8 +- .../MiddlewareClassNameDecorator.php | 8 +- .../EventListenerClassNameNormalizer.php | 2 +- .../Input/Validator/ClassNameValidator.php | 116 ++++++++++++++++-- .../Input/Validator/CommandClassValidator.php | 33 ----- .../Validator/ControllerClassValidator.php | 31 ----- .../Input/Validator/EventClassValidator.php | 33 ----- .../Validator/EventListenerClassValidator.php | 33 ----- .../Input/Validator/ModelClassValidator.php | 27 ---- Configuration/Services.yaml | 6 +- 10 files changed, 115 insertions(+), 182 deletions(-) delete mode 100644 Classes/Command/Input/Validator/CommandClassValidator.php delete mode 100644 Classes/Command/Input/Validator/ControllerClassValidator.php delete mode 100644 Classes/Command/Input/Validator/EventClassValidator.php delete mode 100644 Classes/Command/Input/Validator/EventListenerClassValidator.php delete mode 100644 Classes/Command/Input/Validator/ModelClassValidator.php diff --git a/Classes/Command/Input/Decorator/CommandClassNameDecorator.php b/Classes/Command/Input/Decorator/CommandClassNameDecorator.php index b9edba4f..83a63ee7 100644 --- a/Classes/Command/Input/Decorator/CommandClassNameDecorator.php +++ b/Classes/Command/Input/Decorator/CommandClassNameDecorator.php @@ -11,22 +11,18 @@ namespace FriendsOfTYPO3\Kickstarter\Command\Input\Decorator; -use FriendsOfTYPO3\Kickstarter\Command\Input\Normalizer\CommandClassNameNormalizer; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag('ext-kickstarter.inputHandler.command-class')] readonly class CommandClassNameDecorator implements DecoratorInterface { - public function __construct( - private CommandClassNameNormalizer $commandClassNameNormalizer - ) {} - public function __invoke(?string $defaultValue = null): string { $className = $defaultValue ?? ''; if (str_contains($className, ':')) { $className = substr($className, strpos($className, ':') + 1); } - return $this->commandClassNameNormalizer->__invoke($className); + + return ucfirst($className); } } diff --git a/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php b/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php index 12bd4983..12f26faf 100644 --- a/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php +++ b/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php @@ -11,22 +11,18 @@ namespace FriendsOfTYPO3\Kickstarter\Command\Input\Decorator; -use FriendsOfTYPO3\Kickstarter\Command\Input\Normalizer\MiddlewareClassNameNormalizer; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag('ext-kickstarter.inputHandler.middleware-class')] readonly class MiddlewareClassNameDecorator implements DecoratorInterface { - public function __construct( - private MiddlewareClassNameNormalizer $middlewareClassNameNormalizer - ) {} - public function __invoke(?string $defaultValue = null): string { $className = $defaultValue ?? ''; if (str_contains($className, '/')) { $className = substr($className, strpos($className, '/') + 1); } - return $this->middlewareClassNameNormalizer->__invoke($className); + + return $className; } } diff --git a/Classes/Command/Input/Normalizer/EventListenerClassNameNormalizer.php b/Classes/Command/Input/Normalizer/EventListenerClassNameNormalizer.php index b848250a..bfe38376 100644 --- a/Classes/Command/Input/Normalizer/EventListenerClassNameNormalizer.php +++ b/Classes/Command/Input/Normalizer/EventListenerClassNameNormalizer.php @@ -14,7 +14,7 @@ use FriendsOfTYPO3\Kickstarter\Traits\TryToCorrectClassNameTrait; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; -#[AutoconfigureTag('ext-kickstarter.inputHandler.event-listener-class-name')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.event-listener-class')] class EventListenerClassNameNormalizer implements NormalizerInterface { use TryToCorrectClassNameTrait; diff --git a/Classes/Command/Input/Validator/ClassNameValidator.php b/Classes/Command/Input/Validator/ClassNameValidator.php index cf6c65d1..8bd41c72 100644 --- a/Classes/Command/Input/Validator/ClassNameValidator.php +++ b/Classes/Command/Input/Validator/ClassNameValidator.php @@ -11,21 +11,115 @@ namespace FriendsOfTYPO3\Kickstarter\Command\Input\Validator; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag('ext-kickstarter.inputHandler.command-class')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.controller-class')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.event-class')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.event-listener-class')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.middleware-class')] +#[AutoconfigureTag('ext-kickstarter.inputHandler.model-class')] class ClassNameValidator implements ValidatorInterface { + /** + * List of reserved keywords in PHP that cannot be used as class names (case-insensitive). + * This list is a common compilation of hard-reserved words. + * Some of these are context-sensitive keywords in newer PHP versions but are often + * treated as reserved in the context of class/interface/trait names for compatibility + * or future-proofing. + * Note: 'void', 'iterable', 'object', etc. are not reserved as class names + * in older PHP versions but might be in stricter contexts. + * + * @var array + */ + private const RESERVED_KEYWORDS = [ + '__halt_compiler', + 'abstract', + 'and', + 'array', + 'as', + 'break', + 'callable', + 'case', + 'catch', + 'class', + 'clone', + 'const', + 'continue', + 'declare', + 'default', + 'die', + 'do', + 'echo', + 'else', + 'elseif', + 'empty', + 'enddeclare', + 'endfor', + 'endforeach', + 'endif', + 'endswitch', + 'endwhile', + 'enum', // Added for PHP 8.1+ + 'exit', + 'extends', + 'final', + 'finally', + 'fn', // Added for PHP 7.4+ + 'for', + 'foreach', + 'function', + 'global', + 'goto', + 'if', + 'implements', + 'include', + 'include_once', + 'instanceof', + 'insteadof', + 'interface', + 'isset', + 'list', + 'match', // Added for PHP 8.0+ + 'namespace', + 'new', + 'or', + 'parent', + 'print', + 'private', + 'protected', + 'public', + 'readonly', // Added for PHP 8.1+ + 'require', + 'require_once', + 'return', + 'self', + 'static', + 'switch', + 'throw', + 'trait', + 'try', + 'unset', + 'use', + 'var', + 'while', + 'xor', + 'yield', + ]; + public function __invoke(mixed $answer): string { - if ($answer === null || $answer === '') { - throw new \RuntimeException('Class name must not be empty.', 7856569272); - } - if (preg_match('/^\d/', $answer)) { - throw new \RuntimeException('Class name must not start with a number.', 8716512611); - } - if (preg_match('/[^a-zA-Z0-9]/', $answer)) { - throw new \RuntimeException('Class name contains invalid chars. Please provide just letters and numbers.', 9569056953); - } - if (preg_match('/^[A-Z][a-zA-Z0-9]+$/', $answer) === 0) { - throw new \RuntimeException('Class name must be written in UpperCamelCase like "ProcessRequestEvent".', 8916750461); + $answer = (string)$answer; + + // PHP Class Name Regex from official documentation: https://www.php.net/manual/en/language.oop5.basic.php + $isValidFormat = (bool)preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $answer); + $isReserved = in_array(strtolower($answer), self::RESERVED_KEYWORDS, true); + + if (!$isValidFormat || $isReserved || $answer === '') { + throw new \RuntimeException( + 'The provided class name is not a valid PHP class name or is a reserved keyword.', + 1739132087, + ); } return $answer; diff --git a/Classes/Command/Input/Validator/CommandClassValidator.php b/Classes/Command/Input/Validator/CommandClassValidator.php deleted file mode 100644 index 656e6815..00000000 --- a/Classes/Command/Input/Validator/CommandClassValidator.php +++ /dev/null @@ -1,33 +0,0 @@ -classNameValidator->__invoke($answer); - if (!str_ends_with($answer, self::POSTFIX)) { - throw new \RuntimeException(sprintf('Class name must end with "%s".', self::POSTFIX), 9245301485); - } - return $answer; - } -} diff --git a/Classes/Command/Input/Validator/ControllerClassValidator.php b/Classes/Command/Input/Validator/ControllerClassValidator.php deleted file mode 100644 index 20a5bf08..00000000 --- a/Classes/Command/Input/Validator/ControllerClassValidator.php +++ /dev/null @@ -1,31 +0,0 @@ -classNameValidator->__invoke($answer); - if (!str_ends_with($answer, 'Controller')) { - throw new \RuntimeException('Class name must end with "Controller".', 2791217025); - } - return $answer; - } -} diff --git a/Classes/Command/Input/Validator/EventClassValidator.php b/Classes/Command/Input/Validator/EventClassValidator.php deleted file mode 100644 index 7476461a..00000000 --- a/Classes/Command/Input/Validator/EventClassValidator.php +++ /dev/null @@ -1,33 +0,0 @@ -classNameValidator->__invoke($answer); - if (!str_ends_with($answer, self::POSTFIX)) { - throw new \RuntimeException(sprintf('Class name must end with "%s".', self::POSTFIX), 9245301485); - } - return $answer; - } -} diff --git a/Classes/Command/Input/Validator/EventListenerClassValidator.php b/Classes/Command/Input/Validator/EventListenerClassValidator.php deleted file mode 100644 index 81588f14..00000000 --- a/Classes/Command/Input/Validator/EventListenerClassValidator.php +++ /dev/null @@ -1,33 +0,0 @@ -classNameValidator->__invoke($answer); - if (!str_ends_with($answer, self::POSTFIX)) { - throw new \RuntimeException(sprintf('Class name must end with "%s".', self::POSTFIX), 9245301485); - } - return $answer; - } -} diff --git a/Classes/Command/Input/Validator/ModelClassValidator.php b/Classes/Command/Input/Validator/ModelClassValidator.php deleted file mode 100644 index c3a04fc7..00000000 --- a/Classes/Command/Input/Validator/ModelClassValidator.php +++ /dev/null @@ -1,27 +0,0 @@ -classNameValidator->__invoke($answer); - } -} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 8c368bad..6d1f465d 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -279,6 +279,10 @@ services: arguments: $questions: !tagged_iterator { tag: 'ext-kickstarter.command.question.validator' } + ########################### + ## ASSIGN INPUT HANDLERS ## + ########################### + FriendsOfTYPO3\Kickstarter\Command\Input\Question\Command\CommandAliasQuestion: arguments: $inputHandlers: !tagged_iterator { tag: 'ext-kickstarter.inputHandler.command-name' } @@ -325,7 +329,7 @@ services: FriendsOfTYPO3\Kickstarter\Command\Input\Question\EventListener\EventListenerClassNameQuestion: arguments: - $inputHandlers: !tagged_iterator { tag: 'ext-kickstarter.inputHandler.event-listener-class-name' } + $inputHandlers: !tagged_iterator { tag: 'ext-kickstarter.inputHandler.event-listener-class' } FriendsOfTYPO3\Kickstarter\Command\Input\Question\Locallang\LocallangFileNameQuestion: arguments: From 55ccf54f9faf341a315e9bed22e6a734e8c12ea0 Mon Sep 17 00:00:00 2001 From: Stefan Froemken Date: Mon, 9 Feb 2026 23:24:55 +0100 Subject: [PATCH 2/2] Apply Rector rules to MiddlewareClassNameDecorator --- .../Command/Input/Decorator/MiddlewareClassNameDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php b/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php index 12f26faf..2d133fce 100644 --- a/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php +++ b/Classes/Command/Input/Decorator/MiddlewareClassNameDecorator.php @@ -20,7 +20,7 @@ public function __invoke(?string $defaultValue = null): string { $className = $defaultValue ?? ''; if (str_contains($className, '/')) { - $className = substr($className, strpos($className, '/') + 1); + return substr($className, strpos($className, '/') + 1); } return $className;