Skip to content

Fix class based schemas within routes #724

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ CHANGELOG
[Next release](https://github.com/rebing/graphql-laravel/compare/6.4.0...master)
--------------

### Fixed
- Middleware and methods can be used in class based schemas. [\#724 / jasonvarga](https://github.com/rebing/graphql-laravel/pull/724)\
This is a follow-up fix for [Support for class based schemas](https://github.com/rebing/graphql-laravel/pull/706)

2021-03-31, 6.4.0
-----------------
### Added
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ parameters:
- '/Parameter #1 \$name of method Rebing\\GraphQL\\Support\\Type\:\:getFieldResolver\(\) expects string, int\|string given/'
# tests/Unit/EndpointTest.php
- '/Parameter #1 \$wrappedType of static method GraphQL\\Type\\Definition\\Type::nonNull\(\) expects \(callable\(\): mixed\)\|GraphQL\\Type\\Definition\\NullableType, GraphQL\\Type\\Definition\\Type given/'
- '/Parameter #1 \$array of static method Illuminate\\Support\\Arr::get\(\) expects array\|ArrayAccess/'
# Mass ignore the raw array property access used in many tests for now
# See also https://github.com/nunomaduro/larastan/issues/611
-
Expand Down
33 changes: 32 additions & 1 deletion src/GraphQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,41 @@ protected function getSchemaConfiguration($schema)

$schema = is_array($schema) ? $schema : $this->schemas[$schemaName];

if (! is_string($schema)) {
return static::getNormalizedSchemaConfiguration($schema);
}

/**
* @return array<string, array|Schema>
*/
public static function getNormalizedSchemasConfiguration(): array
{
return array_filter(array_map(function ($schema) {
try {
return static::getNormalizedSchemaConfiguration($schema);
} catch (SchemaNotFound $e) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonvarga do you remember why you added this here?

I was playing around with some refactoring in other parts when I saw this and I wondered: if a schema is broken, aka not found, why would I silence the schema here?

Aka: a broken schema throws an exception for a purpose, it should crash the app?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. 😬

In 6.2.0, if you had an invalid class (or just a string, really, since class names weren't supported yet) it would not throw an exception. I think I just made it continue to not throw an exception to keep the behavior consistent and not a breaking change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I decided to change it (#766) because somehow it didn't make sense to have an invalid configuration being silently skipped (as this is used for generated the routes, you're suddenly left in the dark why a certain schema simply "isn't there").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No arguments here!

return null;
}
}, config('graphql.schemas')));
}

/**
* @param Schema|array<array>|string|null $schema
* @return Schema|array<array>
*/
public static function getNormalizedSchemaConfiguration($schema)
{
if (is_array($schema) || $schema instanceof Schema) {
return $schema;
}

if (is_null($schema)) {
return [];
}

if (! class_exists($schema)) {
throw new SchemaNotFound('Schema class '.$schema.' not found.');
}

/** @var ConfigConvertible $instance */
$instance = app()->make($schema);

Expand Down
9 changes: 6 additions & 3 deletions src/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@
}, ARRAY_FILTER_USE_BOTH);

if ($queryTypesMapWithRoutes) {
$defaultMiddleware = config('graphql.schemas.'.config('graphql.default_schema').'.middleware', []);
$defaultMethod = config('graphql.schemas.'.config('graphql.default_schema').'.method', ['get', 'post']);
$schemaConfig = Rebing\GraphQL\GraphQL::getNormalizedSchemasConfiguration();

$defaultConfig = Arr::get($schemaConfig, config('graphql.default_schema'), []);
$defaultMiddleware = $defaultConfig['middleware'] ?? [];
$defaultMethod = $defaultConfig['method'] ?? ['get', 'post'];

foreach ($queryTypesMapWithRoutes as $type => $info) {
if (preg_match($schemaParameterPattern, $info['route'])) {
Expand All @@ -66,7 +69,7 @@
);
}

foreach (config('graphql.schemas') as $name => $schema) {
foreach ($schemaConfig as $name => $schema) {
foreach (Arr::get($schema, 'method', ['get', 'post']) as $method) {
$routeName = "graphql.$name";
if ($method !== 'get') {
Expand Down
3 changes: 3 additions & 0 deletions tests/Support/Objects/ExampleSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public function toConfig(): array
'mutation' => [
'updateExampleCustom' => UpdateExampleMutation::class,
],
'middleware' => [
ExampleMiddleware::class,
]
];
}
}
13 changes: 13 additions & 0 deletions tests/Support/Objects/ExampleSchemaWithMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Tests\Support\Objects;

class ExampleSchemaWithMethod extends ExampleSchema
{
public function toConfig(): array
{
return array_merge(parent::toConfig(), ['method' => ['post']]);
}
}
4 changes: 2 additions & 2 deletions tests/Unit/GraphQLTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public function testSchemaWithWrongName(): void
*/
public function testSchemaWithInvalidClassName(): void
{
$this->expectException(BindingResolutionException::class);
$this->expectExceptionMessage('Target class [ThisClassDoesntExist] does not exist.');
$this->expectException(SchemaNotFound::class);
$this->expectExceptionMessage('Schema class ThisClassDoesntExist not found.');
GraphQL::schema('invalid_class_based');
}

Expand Down
129 changes: 129 additions & 0 deletions tests/Unit/RoutesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Tests\Unit;

use GraphQL\Utils\BuildSchema;
use Illuminate\Routing\Route;
use Illuminate\Support\Collection;
use Rebing\GraphQL\Tests\Support\Objects\ExampleMiddleware;
use Rebing\GraphQL\Tests\Support\Objects\ExampleSchema;
use Rebing\GraphQL\Tests\Support\Objects\ExampleSchemaWithMethod;
use Rebing\GraphQL\Tests\TestCase;

class RoutesTest extends TestCase
{
protected function getEnvironmentSetUp($app)
{
$app['config']->set('graphql', [

'prefix' => 'graphql_test',

'graphiql' => [
'display' => false,
],

'schemas' => [
'default' => [
'middleware' => [ExampleMiddleware::class]
],
'custom' => [
'middleware' => [ExampleMiddleware::class]
],
'with_methods' => [
'method' => ['post'],
'middleware' => [ExampleMiddleware::class]
],
'class_based' => ExampleSchema::class,
'class_based_with_methods' => ExampleSchemaWithMethod::class,
'shorthand' => BuildSchema::build('
schema {
query: ShorthandExample
}

type ShorthandExample {
echo(message: String!): String!
}
'),
]

]);
}

public function testRoutes(): void
{
$expected = [
'graphql.query' => [
'methods' => ['GET', 'HEAD'],
'uri' => 'graphql_test',
'middleware' => [ExampleMiddleware::class],
],
'graphql.query.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test',
'middleware' => [ExampleMiddleware::class],
],
'graphql.default' => [
'methods' => ['GET', 'HEAD'],
'uri' => 'graphql_test/{default}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.default.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{default}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.custom' => [
'methods' => ['GET', 'HEAD'],
'uri' => 'graphql_test/{custom}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.custom.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{custom}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.with_methods.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{with_methods}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.class_based' => [
'methods' => ['GET', 'HEAD'],
'uri' => 'graphql_test/{class_based}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.class_based.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{class_based}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.class_based_with_methods.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{class_based_with_methods}',
'middleware' => [ExampleMiddleware::class],
],
'graphql.shorthand' => [
'methods' => ['GET', 'HEAD'],
'uri' => 'graphql_test/{shorthand}',
'middleware' => [],
],
'graphql.shorthand.post' => [
'methods' => ['POST'],
'uri' => 'graphql_test/{shorthand}',
'middleware' => [],
],
];

$this->assertEquals($expected, Collection::make(
app('router')->getRoutes()->getRoutesByName()
)->map(function (Route $route) {
return [
'methods' => $route->methods(),
'uri' => $route->uri(),
'middleware' => $route->middleware(),
];
})->all());
}
}