├── LICENSE ├── composer.json ├── ecs.php ├── phpstan-baseline.neon ├── phpstan.neon └── src ├── ConstantExtractor.php ├── ConstantListEnum.php ├── ConstantListTranslatedEnum.php ├── DependencyInjection ├── CompilerPass │ └── TaggedEnumCollectorCompilerPass.php └── EnumExtension.php ├── Enum.php ├── EnumInterface.php ├── EnumRegistry.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php └── LogicException.php ├── Form ├── Extension │ └── EnumTypeGuesser.php └── Type │ └── EnumType.php ├── MyCLabsEnum.php ├── MyCLabsTranslatedEnum.php ├── NativeEnum.php ├── NativeTranslatedEnum.php ├── TranslatedEnum.php ├── Twig └── Extension │ └── EnumExtension.php ├── Validator └── Constraints │ ├── Enum.php │ └── EnumValidator.php └── YokaiEnumBundle.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Yann Eugoné 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yokai/enum-bundle", 3 | "description": "Simple enumeration system with Symfony integration", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Yann Eugoné", 9 | "email": "eugone.yann@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.2", 14 | "symfony/framework-bundle": "^7.0" 15 | }, 16 | "require-dev": { 17 | "doctrine/annotations": "^1.14", 18 | "myclabs/php-enum": "^1.8", 19 | "phpstan/phpstan": "^1.11", 20 | "phpunit/phpunit": "^9.6", 21 | "symfony/form": "^7.0", 22 | "symfony/http-kernel": "^7.0", 23 | "symfony/translation": "^7.0", 24 | "symfony/twig-bundle": "^7.0", 25 | "symfony/validator": "^7.0", 26 | "symfony/yaml": "^7.0", 27 | "symplify/easy-coding-standard": "^12.3", 28 | "twig/twig": "^2.0|^3.0" 29 | }, 30 | "suggest": { 31 | "myclabs/php-enum": "Integrate with enum object", 32 | "symfony/framework-bundle": "Integrate with Symfony Framework", 33 | "symfony/form": "Add enum form type", 34 | "symfony/validator": "Add enum validation", 35 | "twig/twig": "Add enum util functions" 36 | }, 37 | "autoload": { 38 | "psr-4": { "Yokai\\EnumBundle\\": "src" } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Yokai\\EnumBundle\\Tests\\Unit\\": "tests/Unit", 43 | "Yokai\\EnumBundle\\Tests\\Integration\\App\\": "tests/Integration/src", 44 | "Yokai\\EnumBundle\\Tests\\Integration\\": "tests/Integration/tests" 45 | } 46 | }, 47 | "minimum-stability": "stable", 48 | "extra": { 49 | "branch-alias": { 50 | "dev-5.x": "5.x-dev", 51 | "dev-4.x": "4.x-dev", 52 | "dev-3.x": "3.x-dev", 53 | "dev-2.x": "2.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 18 | __DIR__ . '/src', 19 | __DIR__ . '/tests/Integration/src', 20 | __DIR__ . '/tests/Integration/tests', 21 | __DIR__ . '/tests/Unit', 22 | ]); 23 | 24 | $ecsConfig->sets([ 25 | SetList::ARRAY, 26 | SetList::DOCBLOCK, 27 | SetList::NAMESPACES, 28 | SetList::COMMENTS, 29 | SetList::STRICT, 30 | SetList::PSR_12, 31 | ]); 32 | 33 | $ecsConfig->skip([ 34 | /* Do not force array on multiple lines : ['foo' => $foo, 'bar' => $bar] */ 35 | ArrayOpenerAndCloserNewlineFixer::class, 36 | ArrayListItemNewlineFixer::class, 37 | StandaloneLineInMultilineArrayFixer::class, 38 | ]); 39 | 40 | $ecsConfig->ruleWithConfiguration(YodaStyleFixer::class, [ 41 | 'equal' => false, 42 | 'identical' => false, 43 | 'less_and_greater' => false, 44 | ]); 45 | $ecsConfig->ruleWithConfiguration(LineLengthFixer::class, [ 46 | LineLengthFixer::INLINE_SHORT_LINES => false, 47 | ]); 48 | $ecsConfig->ruleWithConfiguration(ForbiddenFunctionsSniff::class, [ 49 | 'forbiddenFunctions' => ['dump' => null, 'dd' => null, 'var_dump' => null, 'die' => null], 50 | ]); 51 | $ecsConfig->ruleWithConfiguration(FunctionDeclarationFixer::class, [ 52 | 'closure_fn_spacing' => 'none', 53 | ]); 54 | $ecsConfig->ruleWithConfiguration(NativeFunctionInvocationFixer::class, [ 55 | 'scope' => 'namespaced', 56 | 'include' => ['@all'], 57 | ]); 58 | }; 59 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" 5 | count: 1 6 | path: src/ConstantExtractor.php 7 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src/ 8 | -------------------------------------------------------------------------------- /src/ConstantExtractor.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class ConstantExtractor 17 | { 18 | /** 19 | * @return list 20 | * @throws LogicException 21 | */ 22 | public static function extract(string $pattern): array 23 | { 24 | [$class, $patternRegex] = self::explode($pattern); 25 | 26 | return self::filter( 27 | self::publicConstants($class, $pattern), 28 | $patternRegex, 29 | $pattern 30 | ); 31 | } 32 | 33 | /** 34 | * @param array $constants 35 | * 36 | * @return list 37 | */ 38 | private static function filter(array $constants, string $regexp, string $pattern): array 39 | { 40 | $matchingNames = \preg_grep($regexp, \array_keys($constants)); 41 | 42 | if ($matchingNames === false || \count($matchingNames) === 0) { 43 | throw LogicException::cannotExtractConstants($pattern, 'Pattern matches no constant.'); 44 | } 45 | 46 | return \array_values(\array_intersect_key($constants, \array_flip($matchingNames))); 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | private static function publicConstants(string $class, string $pattern): array 53 | { 54 | try { 55 | $constants = (new ReflectionClass($class))->getReflectionConstants(); 56 | } catch (ReflectionException $exception) { 57 | throw LogicException::cannotExtractConstants($pattern, \sprintf('Class %s does not exists.', $class)); 58 | } 59 | 60 | $list = []; 61 | foreach ($constants as $constant) { 62 | if (!$constant->isPublic()) { 63 | continue; 64 | } 65 | 66 | $list[$constant->getName()] = $constant->getValue(); 67 | } 68 | 69 | if (\count($list) === 0) { 70 | throw LogicException::cannotExtractConstants( 71 | $pattern, 72 | \sprintf('Class %s has no public constant.', $class) 73 | ); 74 | } 75 | 76 | return $list; 77 | } 78 | 79 | /** 80 | * @return array{string, string} 81 | */ 82 | private static function explode(string $pattern): array 83 | { 84 | if (\substr_count($pattern, '::') !== 1) { 85 | throw LogicException::cannotExtractConstants( 86 | $pattern, 87 | 'Pattern must look like Fully\\Qualified\\ClassName::CONSTANT_*.' 88 | ); 89 | } 90 | 91 | [$class, $constantsNamePattern] = \explode('::', $pattern); 92 | 93 | if (\substr_count($constantsNamePattern, '*') === 0) { 94 | throw LogicException::cannotExtractConstants( 95 | $pattern, 96 | 'Pattern must look like Fully\\Qualified\\ClassName::CONSTANT_*.' 97 | ); 98 | } 99 | 100 | $constantsNameRegexp = \sprintf( 101 | '#^%s$#', 102 | \str_replace('*', '[0-9a-zA-Z_]+', $constantsNamePattern) 103 | ); 104 | 105 | return [$class, $constantsNameRegexp]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ConstantListEnum.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ConstantListEnum extends Enum 13 | { 14 | public function __construct(string $constantsPattern, ?string $name = null) 15 | { 16 | $choices = []; 17 | foreach (ConstantExtractor::extract($constantsPattern) as $value) { 18 | if (!\is_string($value) && !\is_int($value)) { 19 | throw new LogicException( 20 | \sprintf('Extracted constant enum value must be string or int, %s given.', \get_debug_type($value)), 21 | ); 22 | } 23 | $choices[(string)$value] = $value; 24 | } 25 | parent::__construct($choices, $name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ConstantListTranslatedEnum.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ConstantListTranslatedEnum extends TranslatedEnum 13 | { 14 | public function __construct( 15 | string $constantsPattern, 16 | TranslatorInterface $translator, 17 | string $transPattern, 18 | string $transDomain = 'messages', 19 | ?string $name = null 20 | ) { 21 | parent::__construct( 22 | ConstantExtractor::extract($constantsPattern), 23 | $translator, 24 | $transPattern, 25 | $transDomain, 26 | $name 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DependencyInjection/CompilerPass/TaggedEnumCollectorCompilerPass.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class TaggedEnumCollectorCompilerPass implements CompilerPassInterface 15 | { 16 | public function process(ContainerBuilder $container): void 17 | { 18 | if (!$container->hasDefinition('yokai_enum.enum_registry')) { 19 | return; 20 | } 21 | 22 | $registry = $container->getDefinition('yokai_enum.enum_registry'); 23 | 24 | foreach (\array_keys($container->findTaggedServiceIds('yokai_enum.enum')) as $enum) { 25 | $registry->addMethodCall('add', [new Reference($enum)]); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DependencyInjection/EnumExtension.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final class EnumExtension extends Extension 24 | { 25 | public function load(array $configs, ContainerBuilder $container): void 26 | { 27 | $container->register('yokai_enum.enum_registry', EnumRegistry::class); 28 | $container->setAlias(EnumRegistry::class, 'yokai_enum.enum_registry'); 29 | 30 | $registry = new Reference(EnumRegistry::class); 31 | 32 | $requiresForm = \interface_exists(FormInterface::class); 33 | $requiresValidator = \interface_exists(ValidatorInterface::class); 34 | $requiresTwig = \class_exists(TwigBundle::class); 35 | 36 | if ($requiresForm) { 37 | $container->register('yokai_enum.form_type.enum_type', EnumType::class) 38 | ->setArgument('$enumRegistry', $registry) 39 | ->addTag('form.type'); 40 | if ($requiresValidator) { 41 | $container->register('yokai_enum.form_extension.enum_type_guesser', EnumTypeGuesser::class) 42 | ->setArgument('$metadataFactory', new Reference('validator.mapping.class_metadata_factory')) 43 | ->addTag('form.type_guesser'); 44 | } 45 | } 46 | if ($requiresValidator) { 47 | $container->register('yokai_enum.validator_constraints.enum_validator', EnumValidator::class) 48 | ->setArgument('$enumRegistry', $registry) 49 | ->addTag( 50 | 'validator.constraint_validator', 51 | ['alias' => 'yokai_enum.validator_constraints.enum_validator'] 52 | ); 53 | } 54 | if ($requiresTwig) { 55 | $container->register('yokai_enum.twig_extension.enum_extension', EnumTwigExtension::class) 56 | ->setArgument('$registry', $registry) 57 | ->addTag('twig.extension'); 58 | } 59 | 60 | $container->registerForAutoconfiguration(EnumInterface::class) 61 | ->addTag('yokai_enum.enum'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Enum.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Enum implements EnumInterface 14 | { 15 | private string $name; 16 | 17 | /** 18 | * @var array|null 19 | */ 20 | private array|null $choices; 21 | 22 | /** 23 | * @param array|null $choices Allowed to be null if you are extending this class 24 | * and you have overridden the "Enum::build" method. 25 | * @param string|null $name Allowed to be null if you are extending this class 26 | * and you want the FQCN to be the name. 27 | */ 28 | public function __construct(?array $choices, ?string $name = null) 29 | { 30 | if (static::class === __CLASS__ && $choices === null) { 31 | throw new LogicException( 32 | 'When using ' . __CLASS__ . ' directly, $choices argument in ' . __FUNCTION__ . ' method cannot be null' 33 | ); 34 | } 35 | 36 | $this->choices = $choices; 37 | 38 | if ($name === null) { 39 | $name = static::class; 40 | if ( 41 | // using FQCN as name is only allowed for other namespaces 42 | \str_starts_with($name, 'Yokai\\EnumBundle\\') 43 | // except for our tests 44 | && !\str_starts_with($name, 'Yokai\\EnumBundle\\Tests\\') 45 | ) { 46 | throw new LogicException( 47 | 'When using ' . static::class . ', $name argument in ' . __METHOD__ . ' method cannot be null' 48 | ); 49 | } 50 | } 51 | 52 | $this->name = $name; 53 | } 54 | 55 | public function getChoices(): array 56 | { 57 | $this->init(); 58 | 59 | return $this->choices; 60 | } 61 | 62 | public function getValues(): array 63 | { 64 | $this->init(); 65 | 66 | return \array_values($this->choices); 67 | } 68 | 69 | public function getLabel(mixed $value): string 70 | { 71 | $this->init(); 72 | 73 | $label = \array_search($value, $this->choices, false); 74 | if ($label === false) { 75 | throw InvalidArgumentException::enumMissingValue($this, $value); 76 | } 77 | 78 | return $label; 79 | } 80 | 81 | public function getName(): string 82 | { 83 | return $this->name; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | protected function build(): array 90 | { 91 | throw new LogicException(static::class . '::' . __FUNCTION__ . ' should have been overridden.'); 92 | } 93 | 94 | /** 95 | * @phpstan-assert !null $this->choices 96 | */ 97 | private function init(): void 98 | { 99 | if ($this->choices !== null) { 100 | return; 101 | } 102 | 103 | $this->choices = $this->build(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/EnumInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface EnumInterface 11 | { 12 | /** 13 | * Returns enum choices (labels as keys, values as labels) 14 | * 15 | * @return array 16 | */ 17 | public function getChoices(): array; 18 | 19 | /** 20 | * Returns enum values. 21 | * 22 | * @return array 23 | */ 24 | public function getValues(): array; 25 | 26 | /** 27 | * Returns enum value label. 28 | */ 29 | public function getLabel(mixed $value): string; 30 | 31 | /** 32 | * Returns enum identifier (must be unique across app). 33 | */ 34 | public function getName(): string; 35 | } 36 | -------------------------------------------------------------------------------- /src/EnumRegistry.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class EnumRegistry 14 | { 15 | /** 16 | * @var EnumInterface[] 17 | */ 18 | private array $enums = []; 19 | 20 | /** 21 | * @throws LogicException 22 | */ 23 | public function add(EnumInterface $enum): void 24 | { 25 | if ($this->has($enum->getName())) { 26 | throw LogicException::alreadyRegistered($enum->getName()); 27 | } 28 | 29 | $this->enums[$enum->getName()] = $enum; 30 | } 31 | 32 | /** 33 | * @throws InvalidArgumentException 34 | */ 35 | public function get(string $name): EnumInterface 36 | { 37 | if (!$this->has($name)) { 38 | throw InvalidArgumentException::unregisteredEnum($name); 39 | } 40 | 41 | return $this->enums[$name]; 42 | } 43 | 44 | public function has(string $name): bool 45 | { 46 | return isset($this->enums[$name]); 47 | } 48 | 49 | /** 50 | * @return EnumInterface[] 51 | */ 52 | public function all(): array 53 | { 54 | return $this->enums; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 13 | { 14 | public static function unregisteredEnum(string $name): self 15 | { 16 | return new self(\sprintf( 17 | 'Enum with name "%s" was not registered in registry', 18 | $name 19 | )); 20 | } 21 | 22 | public static function enumMissingValue(EnumInterface $enum, mixed $value): self 23 | { 24 | if (\is_object($value) && \method_exists($value, '__toString')) { 25 | $value = (string)$value; 26 | } 27 | if (!\is_string($value)) { 28 | $value = \get_debug_type($value); 29 | } 30 | 31 | return new self(\sprintf( 32 | 'Enum "%s" does not have "%s" value.', 33 | $enum->getName(), 34 | $value 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class EnumTypeGuesser extends ValidatorTypeGuesser 20 | { 21 | public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess 22 | { 23 | $enum = $this->getEnum($constraint); 24 | if ($enum === null) { 25 | return null; 26 | } 27 | 28 | return new TypeGuess( 29 | EnumType::class, 30 | [ 31 | 'enum' => $enum->enum, 32 | 'multiple' => $enum->multiple, 33 | ], 34 | Guess::HIGH_CONFIDENCE 35 | ); 36 | } 37 | 38 | public function guessRequired(string $class, string $property): ?ValueGuess 39 | { 40 | return null; //override parent : not able to guess 41 | } 42 | 43 | public function guessMaxLength(string $class, string $property): ?ValueGuess 44 | { 45 | return null; //override parent : not able to guess 46 | } 47 | 48 | public function guessPattern(string $class, string $property): ?ValueGuess 49 | { 50 | return null; //override parent : not able to guess 51 | } 52 | 53 | private function getEnum(Constraint $constraint): ?Enum 54 | { 55 | if ($constraint instanceof Enum) { 56 | return $constraint; 57 | } 58 | 59 | if ($constraint instanceof Compound) { 60 | foreach ($constraint->constraints as $compositeConstraint) { 61 | $enum = $this->getEnum($compositeConstraint); 62 | if ($enum !== null) { 63 | return $enum; 64 | } 65 | } 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Form/Type/EnumType.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class EnumType extends AbstractType 18 | { 19 | private EnumRegistry $enumRegistry; 20 | 21 | public function __construct(EnumRegistry $enumRegistry) 22 | { 23 | $this->enumRegistry = $enumRegistry; 24 | } 25 | 26 | public function configureOptions(OptionsResolver $resolver): void 27 | { 28 | $resolver 29 | ->setDefined(['enum', 'enum_choice_value']) 30 | ->setRequired('enum') 31 | ->setAllowedValues( 32 | 'enum', 33 | function (string $name): bool { 34 | return $this->enumRegistry->has($name); 35 | } 36 | ) 37 | ->setDefault( 38 | 'choices', 39 | function (Options $options): array { 40 | /** @var string $name */ 41 | $name = $options['enum']; 42 | $choices = $this->enumRegistry->get($name)->getChoices(); 43 | 44 | if ($options['enum_choice_value'] === null) { 45 | foreach ($choices as $value) { 46 | if (!\is_scalar($value)) { 47 | @\trigger_error( 48 | 'Not configuring the "enum_choice_value" option is deprecated.' . 49 | ' It will default to "true" in 5.0.', 50 | \E_USER_DEPRECATED 51 | ); 52 | break; 53 | } 54 | } 55 | } 56 | 57 | return $choices; 58 | } 59 | ) 60 | ->setAllowedTypes('enum_choice_value', ['bool', 'null']) 61 | ->setDefault('enum_choice_value', null) 62 | ->setDefault( 63 | 'choice_value', 64 | static function (Options $options) { 65 | if ($options['enum_choice_value'] !== true) { 66 | return null; 67 | } 68 | 69 | return function ($value) { 70 | if ($value instanceof \BackedEnum) { 71 | return $value->value; 72 | } 73 | if ($value instanceof \UnitEnum) { 74 | return $value->name; 75 | } 76 | if ($value instanceof Enum) { 77 | return $value->getValue(); 78 | } 79 | 80 | return $value; 81 | }; 82 | } 83 | ) 84 | ; 85 | } 86 | 87 | public function getParent(): string 88 | { 89 | return ChoiceType::class; 90 | } 91 | 92 | public function getBlockPrefix(): string 93 | { 94 | return 'yokai_enum'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/MyCLabsEnum.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class MyCLabsEnum extends Enum 14 | { 15 | public function __construct(string $enum, string $name = null) 16 | { 17 | if (!\is_a($enum, ActualMyCLabsEnum::class, true)) { 18 | throw LogicException::invalidMyClabsEnumClass($enum); 19 | } 20 | 21 | if ($name === null && static::class === __CLASS__) { 22 | $name = $enum; 23 | } 24 | 25 | parent::__construct($enum::values(), $name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/MyCLabsTranslatedEnum.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MyCLabsTranslatedEnum extends TranslatedEnum 15 | { 16 | public function __construct( 17 | string $enum, 18 | TranslatorInterface $translator, 19 | string $transPattern, 20 | string $transDomain = 'messages', 21 | string $name = null 22 | ) { 23 | if (!\is_a($enum, ActualMyCLabsEnum::class, true)) { 24 | throw LogicException::invalidMyClabsEnumClass($enum); 25 | } 26 | 27 | if ($name === null && static::class === __CLASS__) { 28 | $name = $enum; 29 | } 30 | 31 | parent::__construct($enum::values(), $translator, $transPattern, $transDomain, $name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NativeEnum.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NativeEnum extends Enum 14 | { 15 | public function __construct(string $enum, string $name = null) 16 | { 17 | if (!\is_a($enum, UnitEnum::class, true)) { 18 | throw LogicException::invalidUnitEnum($enum); 19 | } 20 | 21 | if ($name === null && static::class === __CLASS__) { 22 | $name = $enum; 23 | } 24 | 25 | $choices = []; 26 | foreach ($enum::cases() as $case) { 27 | $choices[$case->name] = $case; 28 | } 29 | 30 | parent::__construct($choices, $name); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NativeTranslatedEnum.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class NativeTranslatedEnum extends TranslatedEnum 15 | { 16 | public function __construct( 17 | string $enum, 18 | TranslatorInterface $translator, 19 | string $transPattern, 20 | string $transDomain = 'messages', 21 | string $name = null 22 | ) { 23 | if (!\is_a($enum, UnitEnum::class, true)) { 24 | throw LogicException::invalidUnitEnum($enum); 25 | } 26 | 27 | if ($name === null && static::class === __CLASS__) { 28 | $name = $enum; 29 | } 30 | 31 | $values = []; 32 | foreach ($enum::cases() as $case) { 33 | $values[$case->name] = $case; 34 | } 35 | 36 | parent::__construct($values, $translator, $transPattern, $transDomain, $name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/TranslatedEnum.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TranslatedEnum extends Enum 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private array $values; 19 | 20 | private TranslatorInterface $translator; 21 | 22 | private string $transPattern; 23 | 24 | private string $transDomain; 25 | 26 | /** 27 | * @param array $values 28 | * 29 | * @throws LogicException 30 | */ 31 | public function __construct( 32 | array $values, 33 | TranslatorInterface $translator, 34 | string $transPattern, 35 | string $transDomain = 'messages', 36 | ?string $name = null 37 | ) { 38 | if (!\str_contains($transPattern, '%s')) { 39 | throw LogicException::placeholderRequired($transPattern); 40 | } 41 | 42 | $this->values = $values; 43 | $this->translator = $translator; 44 | $this->transPattern = $transPattern; 45 | $this->transDomain = $transDomain; 46 | 47 | parent::__construct(null, $name); 48 | } 49 | 50 | protected function build(): array 51 | { 52 | $choices = []; 53 | foreach ($this->values as $key => $value) { 54 | $transLabel = $value; 55 | if (\is_string($key)) { 56 | $transLabel = $key; 57 | } 58 | if (!\is_scalar($transLabel)) { 59 | $transLabel = $key; 60 | } 61 | 62 | $label = $this->translator->trans(\sprintf($this->transPattern, $transLabel), [], $this->transDomain); 63 | $choices[$label] = $value; 64 | } 65 | 66 | return $choices; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Twig/Extension/EnumExtension.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class EnumExtension extends AbstractExtension 16 | { 17 | private EnumRegistry $registry; 18 | 19 | public function __construct(EnumRegistry $registry) 20 | { 21 | $this->registry = $registry; 22 | } 23 | 24 | public function getFunctions(): array 25 | { 26 | return [ 27 | new TwigFunction('enum_values', function ($enum) { 28 | return $this->registry->get($enum)->getValues(); 29 | }), 30 | new TwigFunction('enum_choices', function ($enum) { 31 | return $this->registry->get($enum)->getChoices(); 32 | }), 33 | ]; 34 | } 35 | 36 | public function getFilters(): array 37 | { 38 | return [ 39 | new TwigFilter('enum_label', function ($value, $enum) { 40 | return $this->registry->get($enum)->getLabel($value); 41 | }), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Validator/Constraints/Enum.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)] 13 | final class Enum extends Choice 14 | { 15 | public string $enum; 16 | 17 | /** 18 | * @param array $options 19 | */ 20 | public function __construct( 21 | array $options = [], 22 | string|null $enum = null, 23 | callable|null|string $callback = null, 24 | bool $multiple = null, 25 | bool $strict = null, 26 | int $min = null, 27 | int $max = null, 28 | string $message = null, 29 | string $multipleMessage = null, 30 | string $minMessage = null, 31 | string $maxMessage = null, 32 | array|null $groups = null, 33 | mixed $payload = null, 34 | ) { 35 | if (\is_string($enum)) { 36 | $this->enum = $enum; 37 | } 38 | 39 | // Since Symfony 5.3, first argument of Choice is $options 40 | parent::__construct( 41 | $options, 42 | null, 43 | $callback, 44 | $multiple, 45 | $strict, 46 | $min, 47 | $max, 48 | $message, 49 | $multipleMessage, 50 | $minMessage, 51 | $maxMessage, 52 | $groups, 53 | $payload 54 | ); 55 | } 56 | 57 | public function getDefaultOption(): string 58 | { 59 | return 'enum'; 60 | } 61 | 62 | public function validatedBy(): string 63 | { 64 | return 'yokai_enum.validator_constraints.enum_validator'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Validator/Constraints/EnumValidator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class EnumValidator extends ChoiceValidator 17 | { 18 | private EnumRegistry $enumRegistry; 19 | 20 | public function __construct(EnumRegistry $enumRegistry) 21 | { 22 | $this->enumRegistry = $enumRegistry; 23 | } 24 | 25 | public function validate(mixed $value, Constraint $constraint): void 26 | { 27 | if (!$constraint instanceof Enum) { 28 | throw new UnexpectedTypeException($constraint, Enum::class); 29 | } 30 | 31 | $constraint->choices = null; 32 | $constraint->callback = null; 33 | 34 | if (!isset($constraint->enum)) { 35 | throw new ConstraintDefinitionException('"enum" must be specified on constraint Enum'); 36 | } 37 | 38 | if (!$this->enumRegistry->has($constraint->enum)) { 39 | throw new ConstraintDefinitionException(\sprintf( 40 | '"enum" "%s" on constraint Enum does not exist', 41 | $constraint->enum 42 | )); 43 | } 44 | 45 | $constraint->choices = $this->enumRegistry->get($constraint->enum)->getValues(); 46 | 47 | parent::validate($value, $constraint); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/YokaiEnumBundle.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class YokaiEnumBundle extends Bundle 16 | { 17 | public function build(ContainerBuilder $container): void 18 | { 19 | $container 20 | ->addCompilerPass(new TaggedEnumCollectorCompilerPass()) 21 | ; 22 | } 23 | 24 | public function getContainerExtension(): EnumExtension 25 | { 26 | return new EnumExtension(); 27 | } 28 | } 29 | --------------------------------------------------------------------------------