Before diving head first into code, it's good to familiarize yourself with the
concepts surrounding GraphQL. If you've already experience with GraphQL, feel
free to skip this part.
"schema"
A GraphQL schema defines all the queries, mutations and types
associated with it.
"queries" and "mutations"
The "methods" you call in your GraphQL request (think about your REST endpoint)
"types"
Besides the primitive scalars like int and string, custom "shapes" can be
defined and returned via custom types. They can map to your database models or
basically any data you want to return.
"resolver"
Any time data is returned, it is "resolved". Usually in query/mutations this
specified the primary way to retrieve your data (e.g. using SelectFields or
dataloaders)
Typically, all queries/mutations/types are defined using the $attributes
property and the args() / fields() methods as well as the resolve() method.
args/fields again return a configuration array for each field they supported.
Those fields usually support these shapes
the "key" is the name of the field
type (required): a GraphQL specifier for the type supported here
Optional keys are:
description: made available when introspecting the GraphQL schema
resolve: override the default field resolver
deprecationReason: document why something is deprecated
A word on declaring a field nonNull
It's quite common, and actually good practice, to see the gracious use of
Type::nonNull() on any kind of input and/or output fields.
The more specific the intent of your type system, the better for the consumer.
Some examples
if you require a certain field for a query/mutation argument, declare it non
null
if you know that your (e.g. model) field can never return null (e.g. users ID,
email, etc.), declare it no null
if you return a list of something, like e.g. tags, which is a) always an array
(even empty) and b) shall not contain null values, declare the type like this: Type::nonNull(Type::listOf(Type::nonNull(Type::string())))
There exists a lot of tooling in the GraphQL ecosystem, which benefits the more
specific your type system is.
Data loading
The act of loading/retrieving your data is called "resolving" in GraphQL. GraphQL
itself does not define the "how" and leaves it up to the implementor.
In the context of Laravel it's natural to assume the primary source of data will
be Eloquent. This library therefore provides a convenient helper called
SelectFields which tries its best to
eager load relations and to
avoid n+1 problems.
Be aware that this is not the only way and it's also common to use concepts
called "dataloaders". They usually take advantage of "deferred" executions of
resolved fields, as explained in graphql-php solving n+1 problem.
The gist is that you can use any kind of data source you like (Eloquent,
static data, ElasticSearch results, caching, etc.) in your resolvers but you've
to be mindful of the execution model to avoid repetitive fetches and perform
smart pre-fetching of your data.
GraphiQL
GraphiQL is lightweight "GraphQL IDE" in your browser. It takes advantage of the
GraphQL type system and allows autocompletion of all queries/mutations/types and
fields.
GraphiQL in the meantime evolved in terms of features and complexity, thus for
convenience an older version is directly included with this library.
As enabled by the default configuration, it's available under the /graphiql
route.
If you are using multiple schemas, you can access them via /graphiql/<schema name>.
Middleware Overview
The following middleware concepts are supported:
HTTP middleware (i.e. from Laravel)
GraphQL execution middleware
GraphQL resolver middleware
Briefly said, a middleware usually is a class:
with a handle method
receiving a fixed set of parameters plus a callable for the next middleware
is responsible for calling the "next" middleware
Usually a middleware does just that but may decide to not do that and
just return
has the freedom to mutate the parameters passed on
HTTP middleware
Any Laravel compatible HTTP middleware
can be provided on a global level for all GraphQL endpoints via the config
graphql.route.middleware or on a per-schema basis via
graphql.schemas.<yourschema>.middleware. The per-schema middleware overrides
the global one.
GraphQL execution middleware
The processing of a GraphQL request, henceforth called "execution", flows
through a set of middlewares.
They can be set on global level via graphql.execution_middleware or per-schema
via graphql.schemas.<yourschema>.execution_middleware.
By default, the recommended set of middlewares is provided on the global level.
Note: the execution of the GraphQL request itself is also implemented via a
middleware, which is usually expected to be called last (and does not call
further middlewares). In case you're interested in the details, please see
\Rebing\GraphQL\GraphQL::appendGraphqlExecutionMiddleware
GraphQL resolver middleware
After the HTTP middleware and the execution middleware is applied, the
"resolver middleware" is executed for the query/mutation being targeted
before the actual resolve() method is called.
Schemas are required for defining GraphQL endpoints. You can define multiple
schemas and assign different HTTP middleware and execution middleware to
them, in addition to the global middleware. For example:
'schema' => 'default',
'schemas' => [
'default' => [
'query' => [
ExampleQuery::class,
// It's possible to specify a name/alias with the key// but this is discouraged as it prevents things// like improving performance with e.g. `lazyload_types=true`// It's recommended to specify just the class here and// rely on the `'name'` attribute in the query / type.'someQuery' => AnotherExampleQuery::class,
],
'mutation' => [
ExampleMutation::class,
],
'types' => [
],
],
'user' => [
'query' => [
App\GraphQL\Queries\ProfileQuery::class
],
'mutation' => [
],
'types' => [
],
'middleware' => ['auth'],
// Which HTTP methods to support; must be given in UPPERCASE!'method' => ['GET', 'POST'],
'execution_middleware' => [
\Rebing\GraphQL\Support\ExecutionMiddleware\UnusedVariablesMiddleware::class,
],
],
],
Together with the configuration, in a way the schema defines also the route by
which it is accessible. Per the default configuration of prefix = graphql, the
default schema is accessible via /graphql.
Schema classes
You may alternatively define the configuration of a schema in a class that implements ConfigConvertible.
In your config, you can reference the name of the class, rather than an array.
You can use the php artisan make:graphql:schemaConfig command to create a new schema configuration class automatically.
Creating a query
First you usually create a type you want to return from the query. The Eloquent 'model' is only required if specifying relations.
Note: The selectable key is required, if it's a non-database field or not a relation
namespaceApp\GraphQL\Types;
useApp\User;
useGraphQL\Type\Definition\Type;
useRebing\GraphQL\Support\TypeasGraphQLType;
classUserTypeextendsGraphQLType
{
protected$attributes = [
'name' => 'User',
'description' => 'A user',
// Note: only necessary if you use `SelectFields`'model' => User::class,
];
publicfunctionfields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The id of the user',
// Use 'alias', if the database column is different from the type name.// This is supported for discrete values as well as relations.// - you can also use `DB::raw()` to solve more complex issues// - or a callback returning the value (string or `DB::raw()` result)'alias' => 'user_id',
],
'email' => [
'type' => Type::string(),
'description' => 'The email of user',
'resolve' => function($root, array$args) {
// If you want to resolve the field yourself,// it can be done herereturn strtolower($root->email);
}
],
// Uses the 'getIsMeAttribute' function on our custom User model'isMe' => [
'type' => Type::boolean(),
'description' => 'True, if the queried user is the current user',
'selectable' => false, // Does not try to query this from the database
]
];
}
// You can also resolve a field by declaring a method in the class// with the following format resolve[FIELD_NAME]Field()protectedfunctionresolveEmailField($root, array$args)
{
return strtolower($root->email);
}
}
The best practice is to start with your schema in config/graphql.php and add types directly to your schema (e.g. default):
Adding them on the global level allows to share them between different schemas
but be aware this might make it harder to understand which types/fields are used
where.
or add the type with the GraphQL Facade, in a service provider for example.
And that's it. You should be able to query GraphQL with a request to the url /graphql (or anything you choose in your config). Try a GET request with the following query input
A mutation is like any other query. It accepts arguments and returns an object of a certain type. Mutations are meant to be used for operations modifying (mutating) the state on the server (which queries are not supposed to perform).
This is conventional abstraction, technically you can do anything you want in a query resolve, including mutating state.
For example, a mutation to update the password of a user. First you need to define the Mutation:
请发表评论