diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 3149361a7..706a97d4a 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -15,11 +15,11 @@ jobs: fail-fast: false matrix: php-version: - - 7.4 - - 8.0 - - 8.1 - - 8.2 - - 8.3 + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" steps: - name: Checkout code @@ -29,7 +29,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: none - php-version: ${{ matrix.php-version }} + php-version: "${{ matrix.php-version }}" tools: cs2pr - name: Install dependencies with Composer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f43dc5e45..04097155b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,11 +16,11 @@ jobs: strategy: matrix: php-version: - - 7.4 - - 8.0 - - 8.1 - - 8.2 - - 8.3 + - "7.4" + - "8.0" + - "8.1" + - "8.2" + - "8.3" dependencies: - highest include: @@ -36,7 +36,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-version }} + php-version: "${{ matrix.php-version }}" coverage: pcov - name: Install dependencies with Composer diff --git a/composer.json b/composer.json index 8422157fd..efc1c5e58 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ ], "php-cs-fixer": "php-cs-fixer fix", "rector": "rector process", - "stan": "phpstan", + "stan": "phpstan --verbose", "test": "php -d zend.exception_ignore_args=Off -d zend.assertions=On -d assert.active=On -d assert.exception=On vendor/bin/phpunit" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2ceab9dc1..4fd2c394e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -14,15 +14,6 @@ parameters: missingCheckedExceptionInThrows: true tooWideThrowType: true - excludePaths: - # PHP 8 attributes - - src/Type/Definition/Deprecated.php - - src/Type/Definition/Description.php - # PHP 8.1 enums - - src/Type/Definition/PhpEnumType.php - - tests/Type/PhpEnumTypeTest.php - - tests/Type/PhpEnumType - ignoreErrors: # Since this is a library that is supposed to be flexible, we don't # want to lock down every possible extension point. @@ -33,10 +24,9 @@ parameters: - "~Variable method call on GraphQL\\\\Language\\\\Parser\\.~" # Useful/necessary when dealing with arbitrary user data - - - message: "~Variable property access on object~" - path: src/Utils/Utils.php - count: 2 + - message: "~Variable property access on object~" + path: src/Utils/Utils.php + count: 2 # PHPStan does not play nicely with markTestSkipped() - message: "~Unreachable statement - code above always terminates~" @@ -60,19 +50,18 @@ includes: - phpstan/include-by-php-version.php services: - - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsAbstractTypeStaticMethodTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsCompositeTypeStaticMethodTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsInputTypeStaticMethodTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension - - - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsOutputTypeStaticMethodTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsAbstractTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + + - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsCompositeTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + + - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsInputTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + + - class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsOutputTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/phpstan/include-by-php-version.php b/phpstan/include-by-php-version.php index 679a97437..a9fa1354e 100644 --- a/phpstan/include-by-php-version.php +++ b/phpstan/include-by-php-version.php @@ -2,8 +2,15 @@ $includes = []; -if (PHP_VERSION_ID >= 80200) { - $includes[] = __DIR__ . '/php-82.neon'; +$phpversion = phpversion(); +if (version_compare($phpversion, '8.2', '>=')) { + $includes[] = __DIR__ . '/php-at-least-8.2.neon'; +} +if (version_compare($phpversion, '8.1', '<')) { + $includes[] = __DIR__ . '/php-below-8.1.neon'; +} +if (version_compare($phpversion, '8.0', '<')) { + $includes[] = __DIR__ . '/php-below-8.0.neon'; } $config = []; diff --git a/phpstan/php-82.neon b/phpstan/php-at-least-8.2.neon similarity index 100% rename from phpstan/php-82.neon rename to phpstan/php-at-least-8.2.neon diff --git a/phpstan/php-below-8.0.neon b/phpstan/php-below-8.0.neon new file mode 100644 index 000000000..13f9ca382 --- /dev/null +++ b/phpstan/php-below-8.0.neon @@ -0,0 +1,9 @@ +parameters: + excludePaths: + # PHP 8 attributes + - ../src/Type/Definition/Deprecated.php + - ../src/Type/Definition/Description.php + ignoreErrors: + # Native enums require PHP 8.1, but checking if a value is of an unknown class still works + - path: ../src/Type/Definition/EnumType.php + identifier: class.notFound diff --git a/phpstan/php-below-8.1.neon b/phpstan/php-below-8.1.neon new file mode 100644 index 000000000..caa3e0f61 --- /dev/null +++ b/phpstan/php-below-8.1.neon @@ -0,0 +1,7 @@ +parameters: + excludePaths: + # PHP 8.1 enums + - ../src/Type/Definition/PhpEnumType.php + - ../tests/Type/EnumTypeTest.php + - ../tests/Type/PhpEnumTypeTest.php + - ../tests/Type/PhpEnumType diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 57e31f740..087788728 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -136,6 +136,14 @@ public function serialize($value) return $lookup[$value]->name; } + if (is_a($value, \BackedEnum::class)) { + return $value->value; + } + + if (is_a($value, \UnitEnum::class)) { + return $value->name; + } + $safeValue = Utils::printSafe($value); throw new SerializationError("Cannot serialize value as enum: {$safeValue}"); } diff --git a/src/Type/Definition/PhpEnumType.php b/src/Type/Definition/PhpEnumType.php index e61736efa..19e2d84d5 100644 --- a/src/Type/Definition/PhpEnumType.php +++ b/src/Type/Definition/PhpEnumType.php @@ -18,6 +18,9 @@ class PhpEnumType extends EnumType /** * @param class-string<\UnitEnum> $enum * @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class + * + * @throws \Exception + * @throws \ReflectionException */ public function __construct(string $enum, ?string $name = null) { @@ -82,6 +85,11 @@ protected function baseName(string $class): string return end($parts); } + /** + * @param \ReflectionClassConstant|\ReflectionClass<\UnitEnum> $reflection + * + * @throws \Exception + */ protected function extractDescription(\ReflectionClassConstant|\ReflectionClass $reflection): ?string { $attributes = $reflection->getAttributes(Description::class); @@ -100,6 +108,7 @@ protected function extractDescription(\ReflectionClassConstant|\ReflectionClass return PhpDoc::unwrap($unpadded); } + /** @throws \Exception */ protected function deprecationReason(\ReflectionClassConstant $reflection): ?string { $attributes = $reflection->getAttributes(Deprecated::class); diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index 919dfabff..597f917f8 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -5,7 +5,10 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use GraphQL\Error\DebugFlag; use GraphQL\GraphQL; +use GraphQL\Language\Parser; use GraphQL\Language\SourceLocation; +use GraphQL\Tests\Type\PhpEnumType\BackedPhpEnum; +use GraphQL\Tests\Type\PhpEnumType\PhpEnum; use GraphQL\Tests\Type\TestClasses\OtherEnumType; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; @@ -13,6 +16,7 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; +use GraphQL\Utils\BuildSchema; use PHPUnit\Framework\TestCase; final class EnumTypeTest extends TestCase @@ -661,4 +665,62 @@ public function testLazilyDefineValuesAsCallable(): void // @phpstan-ignore-next-line $called is mutated self::assertSame(1, $called, 'Should call enum values callable exactly once'); } + + public function testSerializesNativeBackedEnums(): void + { + if (version_compare(phpversion(), '8.1', '<')) { + self::markTestSkipped('Native PHP enums are only available with PHP 8.1'); + } + + $documentNode = Parser::parse(<<<'SDL' + type Query { + phpEnum(fromEnum: PhpEnum!): PhpEnum! + } + + enum PhpEnum { + A + B + C + } + SDL); + + $this->schema = BuildSchema::build($documentNode); + $resolvers = [ + 'phpEnum' => fn (): BackedPhpEnum => BackedPhpEnum::A, + ]; + + self::assertSame( + ['data' => ['phpEnum' => 'A']], + GraphQL::executeQuery($this->schema, '{ phpEnum(fromEnum: A) }', $resolvers)->toArray() + ); + } + + public function testSerializesNativeUnitEnums(): void + { + if (version_compare(phpversion(), '8.1', '<')) { + self::markTestSkipped('Native PHP enums are only available with PHP 8.1'); + } + + $documentNode = Parser::parse(<<<'SDL' + type Query { + phpEnum(fromEnum: PhpEnum!): PhpEnum! + } + + enum PhpEnum { + A + B + C + } + SDL); + + $this->schema = BuildSchema::build($documentNode); + $resolvers = [ + 'phpEnum' => fn (): PhpEnum => PhpEnum::B, + ]; + + self::assertSame( + ['data' => ['phpEnum' => 'B']], + GraphQL::executeQuery($this->schema, '{ phpEnum(fromEnum: B) }', $resolvers)->toArray() + ); + } } diff --git a/tests/Type/PhpEnumType/BackedPhpEnum.php b/tests/Type/PhpEnumType/BackedPhpEnum.php new file mode 100644 index 000000000..3a6293c8a --- /dev/null +++ b/tests/Type/PhpEnumType/BackedPhpEnum.php @@ -0,0 +1,10 @@ +