diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1b802f6..a1fc2f3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,7 +80,7 @@ jobs: - name: "Setup PHP" uses: shivammathur/setup-php@v2 with: - coverage: xdebug + coverage: none php-version: '8.3' - name: "Install dependencies with composer" @@ -88,3 +88,23 @@ jobs: - name: "Run checkstyle with symplify/easy-coding-standard" run: vendor/bin/ecs + + phpstan: + name: "PHPStan" + runs-on: ubuntu-latest + + steps: + - name: "Checkout" + uses: actions/checkout@v2 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: '8.3' + + - name: "Install dependencies with composer" + run: composer update --no-interaction --no-progress + + - name: "Run static analysis with phpstan/phpstan" + run: vendor/bin/phpstan diff --git a/composer.json b/composer.json index 2404e8b..b65c703 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "require-dev": { "doctrine/annotations": "^1.14", "myclabs/php-enum": "^1.8", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^9.6", "symfony/form": "^7.0", "symfony/http-kernel": "^7.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..656b179 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|T of object, string given\\.$#" + count: 1 + path: src/ConstantExtractor.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..3babcbd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + paths: + - src/ diff --git a/src/ConstantExtractor.php b/src/ConstantExtractor.php index bbe2fd3..a0ab2fe 100644 --- a/src/ConstantExtractor.php +++ b/src/ConstantExtractor.php @@ -16,6 +16,7 @@ final class ConstantExtractor { /** + * @return list * @throws LogicException */ public static function extract(string $pattern): array @@ -29,17 +30,25 @@ public static function extract(string $pattern): array ); } + /** + * @param array $constants + * + * @return list + */ private static function filter(array $constants, string $regexp, string $pattern): array { $matchingNames = \preg_grep($regexp, \array_keys($constants)); - if (\count($matchingNames) === 0) { + if ($matchingNames === false || \count($matchingNames) === 0) { throw LogicException::cannotExtractConstants($pattern, 'Pattern matches no constant.'); } return \array_values(\array_intersect_key($constants, \array_flip($matchingNames))); } + /** + * @return array + */ private static function publicConstants(string $class, string $pattern): array { try { @@ -67,6 +76,9 @@ private static function publicConstants(string $class, string $pattern): array return $list; } + /** + * @return array{string, string} + */ private static function explode(string $pattern): array { if (\substr_count($pattern, '::') !== 1) { diff --git a/src/ConstantListEnum.php b/src/ConstantListEnum.php index c7b601b..34cbbde 100644 --- a/src/ConstantListEnum.php +++ b/src/ConstantListEnum.php @@ -4,6 +4,8 @@ namespace Yokai\EnumBundle; +use Yokai\EnumBundle\Exception\LogicException; + /** * @author Yann Eugoné */ @@ -11,7 +13,15 @@ class ConstantListEnum extends Enum { public function __construct(string $constantsPattern, ?string $name = null) { - $values = ConstantExtractor::extract($constantsPattern); - parent::__construct(\array_combine($values, $values), $name); + $choices = []; + foreach (ConstantExtractor::extract($constantsPattern) as $value) { + if (!\is_string($value) && !\is_int($value)) { + throw new LogicException( + \sprintf('Extracted constant enum value must be string or int, %s given.', \get_debug_type($value)), + ); + } + $choices[(string)$value] = $value; + } + parent::__construct($choices, $name); } } diff --git a/src/Enum.php b/src/Enum.php index a50ef86..a262681 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -92,6 +92,9 @@ protected function build(): array throw new LogicException(static::class . '::' . __FUNCTION__ . ' should have been overridden.'); } + /** + * @phpstan-assert !null $this->choices + */ private function init(): void { if ($this->choices !== null) { diff --git a/src/Form/Extension/EnumTypeGuesser.php b/src/Form/Extension/EnumTypeGuesser.php index fa2cf6e..6946536 100644 --- a/src/Form/Extension/EnumTypeGuesser.php +++ b/src/Form/Extension/EnumTypeGuesser.php @@ -35,17 +35,17 @@ public function guessTypeForConstraint(Constraint $constraint): ?TypeGuess ); } - public function guessRequired($class, $property): ?ValueGuess + public function guessRequired(string $class, string $property): ?ValueGuess { return null; //override parent : not able to guess } - public function guessMaxLength($class, $property): ?ValueGuess + public function guessMaxLength(string $class, string $property): ?ValueGuess { return null; //override parent : not able to guess } - public function guessPattern($class, $property): ?ValueGuess + public function guessPattern(string $class, string $property): ?ValueGuess { return null; //override parent : not able to guess } diff --git a/src/Form/Type/EnumType.php b/src/Form/Type/EnumType.php index cb918ec..53ef7c9 100644 --- a/src/Form/Type/EnumType.php +++ b/src/Form/Type/EnumType.php @@ -40,7 +40,9 @@ function (string $name): bool { ->setDefault( 'choices', function (Options $options): array { - $choices = $this->enumRegistry->get($options['enum'])->getChoices(); + /** @var string $name */ + $name = $options['enum']; + $choices = $this->enumRegistry->get($name)->getChoices(); if ($options['enum_choice_value'] === null) { foreach ($choices as $value) { diff --git a/src/TranslatedEnum.php b/src/TranslatedEnum.php index 76bde36..c1069ce 100644 --- a/src/TranslatedEnum.php +++ b/src/TranslatedEnum.php @@ -13,7 +13,7 @@ class TranslatedEnum extends Enum { /** - * @var array + * @var array */ private $values; @@ -64,6 +64,9 @@ protected function build(): array if (\is_string($key)) { $transLabel = $key; } + if (!\is_scalar($transLabel)) { + $transLabel = $key; + } $label = $this->translator->trans(\sprintf($this->transPattern, $transLabel), [], $this->transDomain); $choices[$label] = $value; diff --git a/src/Validator/Constraints/Enum.php b/src/Validator/Constraints/Enum.php index 3cfe2db..2db9fc9 100644 --- a/src/Validator/Constraints/Enum.php +++ b/src/Validator/Constraints/Enum.php @@ -20,9 +20,13 @@ final class Enum extends Choice */ public $enum; + /** + * @param array $options + */ public function __construct( - $enum = null, - $callback = null, + array $options = [], + string|null $enum = null, + callable|null|string $callback = null, bool $multiple = null, bool $strict = null, int $min = null, @@ -31,57 +35,29 @@ public function __construct( string $multipleMessage = null, string $minMessage = null, string $maxMessage = null, - $groups = null, - $payload = null, - array $options = [] + array|null $groups = null, + mixed $payload = null, ) { - if (\is_array($enum)) { - // Symfony 4.4 Constraints has single constructor argument containing all options - parent::__construct($enum); - } else { - if (\is_string($enum)) { - $this->enum = $enum; - } - // Symfony 5.x Constraints has many constructor arguments for PHP 8.0 Attributes support - - $firstConstructorArg = (new \ReflectionClass(Choice::class)) - ->getConstructor()->getParameters()[0]->getName(); - if ($firstConstructorArg === 'choices') { - // Prior to Symfony 5.3, first argument of Choice was $choices - parent::__construct( - null, - $callback, - $multiple, - $strict, - $min, - $max, - $message, - $multipleMessage, - $minMessage, - $maxMessage, - $groups, - $payload, - $options - ); - } else { - // Since Symfony 5.3, first argument of Choice is $options - parent::__construct( - $options, - null, - $callback, - $multiple, - $strict, - $min, - $max, - $message, - $multipleMessage, - $minMessage, - $maxMessage, - $groups, - $payload - ); - } + if (\is_string($enum)) { + $this->enum = $enum; } + + // Since Symfony 5.3, first argument of Choice is $options + parent::__construct( + $options, + null, + $callback, + $multiple, + $strict, + $min, + $max, + $message, + $multipleMessage, + $minMessage, + $maxMessage, + $groups, + $payload + ); } public function getDefaultOption(): string diff --git a/src/Validator/Constraints/EnumValidator.php b/src/Validator/Constraints/EnumValidator.php index 05773f1..7e01408 100644 --- a/src/Validator/Constraints/EnumValidator.php +++ b/src/Validator/Constraints/EnumValidator.php @@ -25,7 +25,7 @@ public function __construct(EnumRegistry $enumRegistry) $this->enumRegistry = $enumRegistry; } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$constraint instanceof Enum) { throw new UnexpectedTypeException($constraint, Enum::class); diff --git a/tests/Unit/ConstantListEnumTest.php b/tests/Unit/ConstantListEnumTest.php index 68e0299..385dbb1 100644 --- a/tests/Unit/ConstantListEnumTest.php +++ b/tests/Unit/ConstantListEnumTest.php @@ -67,4 +67,10 @@ public function testLabelNotFound(): void $enum->getLabel('unknown'); } + + public function testConstantMustBeStringOrInt(): void + { + $this->expectException(LogicException::class); + new ConstantListEnum(Vehicle::class . '::PERIOD_*', 'vehicle.period'); + } } diff --git a/tests/Unit/Fixtures/Vehicle.php b/tests/Unit/Fixtures/Vehicle.php index dc78603..9b13d85 100644 --- a/tests/Unit/Fixtures/Vehicle.php +++ b/tests/Unit/Fixtures/Vehicle.php @@ -25,4 +25,8 @@ class Vehicle public const WHEELS_TWO = 2; public const WHEELS_FOUR = 4; public const WHEELS_EIGHT = 8; + + public const PERIOD_COLLECTION = [1817, 1980]; + public const PERIOD_OLD = [1981, 2010]; + public const PERIOD_RECENT = [2010, 2024]; } diff --git a/tests/Unit/Form/Extension/EnumTypeGuesserTest.php b/tests/Unit/Form/Extension/EnumTypeGuesserTest.php index 2e5302a..a06da63 100644 --- a/tests/Unit/Form/Extension/EnumTypeGuesserTest.php +++ b/tests/Unit/Form/Extension/EnumTypeGuesserTest.php @@ -70,7 +70,7 @@ protected function getConstraints(array $options): array ->with(self::TEST_CLASS) ->willReturn($metadata); - $this->guesser = new EnumTypeGuesser($this->metadataFactory, $this->enumRegistry); + $this->guesser = new EnumTypeGuesser($this->metadataFactory); parent::setUp(); } diff --git a/tests/Unit/Validator/Constraints/EnumValidatorTest.php b/tests/Unit/Validator/Constraints/EnumValidatorTest.php index 0ebabea..0c9e99e 100644 --- a/tests/Unit/Validator/Constraints/EnumValidatorTest.php +++ b/tests/Unit/Validator/Constraints/EnumValidatorTest.php @@ -46,19 +46,19 @@ public function testEnumIsRequired(): void public function testValidEnumIsRequired(): void { $this->expectException(ConstraintDefinitionException::class); - $this->validator->validate('foo', new Enum('state')); + $this->validator->validate('foo', new Enum(enum: 'state')); } public function testNullIsValid(): void { - $this->validator->validate(null, new Enum('type')); + $this->validator->validate(null, new Enum(enum: 'type')); $this->assertNoViolation(); } public function testValidSingleEnum(): void { - $this->validator->validate('customer', new Enum('type')); + $this->validator->validate('customer', new Enum(enum: 'type')); $this->assertNoViolation(); }