diff --git a/Classes/Command/Input/Decorator/CommandClassNameDecorator.php b/Classes/Command/Input/Decorator/CommandClassNameDecorator.php index b9edba4..83a63ee 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 12bd498..2d133fc 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 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 b848250..bfe3837 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 cf6c65d..8bd41c7 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 656e681..0000000 --- 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 20a5bf0..0000000 --- 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 7476461..0000000 --- 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 81588f1..0000000 --- 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 c3a04fc..0000000 --- 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 8c368ba..6d1f465 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: