diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d69135c..096fb35 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,11 +12,30 @@ concurrency: jobs: test: runs-on: ubuntu-latest - name: test + name: test (${{ matrix.name }}) strategy: matrix: - install-args: [''] - php-version: ['8.2', '8.3', '8.4'] + include: + - name: php-8.2 + php-version: '8.2' + install-args: '' + composer-cache-suffix: default + - name: php-8.3 + php-version: '8.3' + install-args: '' + composer-cache-suffix: default + - name: php-8.4 + php-version: '8.4' + install-args: '' + composer-cache-suffix: default + - name: php-8.5 + php-version: '8.5' + install-args: '' + composer-cache-suffix: default + - name: php-8.5-prefer-lowest + php-version: '8.5' + install-args: '--prefer-lowest' + composer-cache-suffix: prefer-lowest fail-fast: false steps: - name: checkout @@ -37,9 +56,9 @@ jobs: uses: actions/cache@v5.0.5 with: path: ${{ steps.composercache.outputs.dir }} - key: composer-${{ hashFiles('**/composer.json') }}-${{ matrix.install-args }} + key: composer-${{ hashFiles('**/composer.json') }}-${{ matrix.composer-cache-suffix }} restore-keys: | - composer-${{ hashFiles('**/composer.json') }}-${{ matrix.install-args }} + composer-${{ hashFiles('**/composer.json') }}-${{ matrix.composer-cache-suffix }} composer-${{ hashFiles('**/composer.json') }}- composer- @@ -51,15 +70,44 @@ jobs: run: | vendor/bin/simple-phpunit --no-coverage + static-analysis: + runs-on: ubuntu-latest + name: static-analysis + steps: + - name: checkout + uses: actions/checkout@v6 + + - name: php + uses: shivammathur/setup-php@2.36.0 + with: + php-version: '8.5' + ini-values: zend.assertions=1 + + - name: composer-cache-dir + id: composercache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: composer-cache + uses: actions/cache@v5.0.5 + with: + path: ${{ steps.composercache.outputs.dir }} + key: composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-${{ hashFiles('**/composer.json') }} + composer- + + - name: composer + run: | + composer update --no-interaction --no-progress --prefer-dist + - name: phpstan-cache uses: actions/cache@v5.0.5 with: - key: phpstan-${{ matrix.php-version }}-${{ matrix.install-args }}-${{ github.ref }}-${{ github.sha }} + key: phpstan-${{ github.ref }}-${{ github.sha }} path: .phpstan-cache restore-keys: | - phpstan-${{ matrix.php-version }}-${{ matrix.install-args }}-${{ github.ref }}- - phpstan-${{ matrix.php-version }}-${{ matrix.install-args }}- - phpstan-${{ matrix.php-version }}- + phpstan-${{ github.ref }}- phpstan- - name: phpstan diff --git a/composer.json b/composer.json index e457869..5af612a 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require" : { "php" : ">=8.1", "ext-json": "*", - "thecodingmachine/graphqlite" : "^8", + "thecodingmachine/graphqlite" : "^8.1", "thecodingmachine/graphqlite-symfony-validator-bridge": "^7.1.1", "symfony/config": "^6.4 || ^7.0 || ^8.0", "symfony/console": "^6.4 || ^7.0 || ^8.0", @@ -34,7 +34,7 @@ "symfony/security-bundle": "^6.4 || ^7.0 || ^8.0", "symfony/yaml": "^6.4 || ^7.0 || ^8.0", "beberlei/porpaginas": "^1.2 || ^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7 || ^8.0", + "symfony/phpunit-bridge": "^6.4.35 || ^7 || ^8.0", "phpstan/phpstan": "^2", "phpstan/phpstan-symfony": "^2.0", "composer/package-versions-deprecated": "^1.8", @@ -42,10 +42,7 @@ "overblog/graphiql-bundle": "^0.2 || ^0.3 || ^1" }, "conflict": { - "symfony/event-dispatcher": "<4.3", - "symfony/security-core": "<4.3", - "symfony/routing": "<4.3", - "phpdocumentor/type-resolver": "<1.4" + "symfony/password-hasher": "<6.4" }, "scripts": { "phpstan": "phpstan analyse -c phpstan.neon --no-progress" diff --git a/phpstan.neon b/phpstan.neon index 544bd75..dfc0b56 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,8 +9,8 @@ parameters: - src excludePaths: - vendor - - cache - - .phpstan-cache + - cache (?) + - .phpstan-cache (?) - tests polluteScopeWithLoopInitialAssignments: false diff --git a/src/DependencyInjection/GraphQLiteCompilerPass.php b/src/DependencyInjection/GraphQLiteCompilerPass.php index 75246d6..c254004 100644 --- a/src/DependencyInjection/GraphQLiteCompilerPass.php +++ b/src/DependencyInjection/GraphQLiteCompilerPass.php @@ -337,13 +337,16 @@ private function makePublicInjectedServices(ReflectionClass $refClass, Annotatio $services = $this->getCodeCache()->get($refClass, function() use ($refClass, $reader, $container, $isController): array { $services = []; foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - $field = $reader->getRequestAnnotation($method, Field::class) ?? $reader->getRequestAnnotation($method, Query::class) ?? $reader->getRequestAnnotation($method, Mutation::class); - if ($field !== null) { + $resolveResult = $this->resolveFieldGraphqlElement($reader, $method); + if ($resolveResult['exists']) { if ($isController) { $services[$refClass->getName()] = $refClass->getName(); } + $services += $this->getListOfInjectedServices($method, $container); - if ($field instanceof Field && $field->getPrefetchMethod() !== null) { + + $field = $resolveResult['field']; + if (null !== $field && $field->getPrefetchMethod() !== null) { $services += $this->getListOfInjectedServices($refClass->getMethod($field->getPrefetchMethod()), $container); } } @@ -467,4 +470,29 @@ private function getClassList(string $namespace): Generator } } + /** + * @return array{exists: bool, field: Field|null} + */ + private function resolveFieldGraphqlElement(AnnotationReader $reader, ReflectionMethod $method): array + { + // backward compatibility with graphqlite v8.1 + // @phpstan-ignore function.alreadyNarrowedType + if (false === \method_exists($reader, 'getGraphQLElementAnnotation')) { + // graphqlite versions in this BC branch expose getRequestAnnotation instead. + // @phpstan-ignore method.notFound + $element = $reader->getRequestAnnotation($method, Field::class) + // @phpstan-ignore method.notFound + ?? $reader->getRequestAnnotation($method, Query::class) + // @phpstan-ignore method.notFound + ?? $reader->getRequestAnnotation($method, Mutation::class); + + return ['exists' => null !== $element, 'field' => ($element instanceof Field ? $element : null)]; + } + + $element = $reader->getGraphQLElementAnnotation($method, Field::class) + ?? $reader->getGraphQLElementAnnotation($method, Query::class) + ?? $reader->getGraphQLElementAnnotation($method, Mutation::class); + + return ['exists' => null !== $element, 'field' => ($element instanceof Field ? $element : null)]; + } } diff --git a/tests/Command/DumpSchemaCommandTest.php b/tests/Command/DumpSchemaCommandTest.php index e99fc48..17cb934 100644 --- a/tests/Command/DumpSchemaCommandTest.php +++ b/tests/Command/DumpSchemaCommandTest.php @@ -18,9 +18,20 @@ public function testExecute(): void $commandTester = new CommandTester($command); $commandTester->execute([]); + preg_match('/type Product \{(?P[\s\S]*?)}/', $commandTester->getDisplay(), $matches); + self::assertArrayHasKey('body', $matches); + + self::assertMatchesRegularExpression( + '/name: String!/', + $matches['body'] + ); + self::assertMatchesRegularExpression( + '/price: Float!/', + $matches['body'] + ); self::assertMatchesRegularExpression( - '/type Product {[\s"]*name: String!\s*price: Float!\s*seller: Contact\s*}/', - $commandTester->getDisplay() + '/seller: Contact/', + $matches['body'] ); } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index b5ecc32..cdfe673 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -481,7 +481,10 @@ public function testValidation(): void $response = $kernel->handle($request); + $this->assertSame(400, $response->getStatusCode()); $result = json_decode($response->getContent(), true); + $this->assertIsArray($result); + $this->assertArrayHasKey('errors', $result); $errors = $result['errors']; $this->assertSame('This value is not a valid email address.', $errors[0]['message']); diff --git a/tests/GraphQLiteTestingKernel.php b/tests/GraphQLiteTestingKernel.php index 16e358c..def5347 100644 --- a/tests/GraphQLiteTestingKernel.php +++ b/tests/GraphQLiteTestingKernel.php @@ -100,9 +100,11 @@ public function registerBundles(): iterable public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) { $loader->load(function(ContainerBuilder $container) { - $frameworkConf = array( - 'secret' => 'S0ME_SECRET' - ); + $frameworkConf = [ + 'secret' => 'S0ME_SECRET', + // forward compatibility with symfony/validator v7 + 'validation' => ['email_validation_mode' => 'html5'], + ]; $frameworkConf['cache'] =[ 'app' => 'cache.adapter.array', @@ -126,46 +128,52 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) $extraConfig['enable_authenticator_manager'] = true; } - $container->loadFromExtension('security', array_merge(array( - 'providers' => [ - 'in_memory' => [ - 'memory' => [ - 'users' => [ - 'foo' => [ - 'password' => 'bar', - 'roles' => 'ROLE_USER', + $container->loadFromExtension( + 'security', + array_merge( + [ + 'providers' => [ + 'in_memory' => [ + 'memory' => [ + 'users' => [ + 'foo' => [ + 'password' => 'bar', + 'roles' => 'ROLE_USER', + ], + ], ], - ], - ], - ], - 'in_memory_other' => [ - 'memory' => [ - 'users' => [ - 'foo' => [ - 'password' => 'bar', - 'roles' => 'ROLE_USER', + ], + 'in_memory_other' => [ + 'memory' => [ + 'users' => [ + 'foo' => [ + 'password' => 'bar', + 'roles' => 'ROLE_USER', + ], + ], ], ], ], + 'firewalls' => [ + 'main' => [ + 'provider' => 'in_memory', + ], + ], + 'password_hashers' => [ + InMemoryUser::class => 'plaintext', + ], ], - ], - 'firewalls' => [ - 'main' => [ - 'provider' => 'in_memory' - ] - ], - 'password_hashers' => [ - InMemoryUser::class => 'plaintext', - ], - ), $extraConfig)); + $extraConfig, + ), + ); } - $graphqliteConf = array( + $graphqliteConf = [ 'namespace' => [ 'controllers' => $this->controllersNamespace, 'types' => $this->typesNamespace ], - ); + ]; if ($this->enableLogin) { $graphqliteConf['security']['enable_login'] = $this->enableLogin;