• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

rebing/graphql-laravel: Laravel wrapper for Facebook's GraphQL

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

rebing/graphql-laravel

开源软件地址(OpenSource Url):

https://github.com/rebing/graphql-laravel

开源编程语言(OpenSource Language):

PHP 99.7%

开源软件介绍(OpenSource Introduction):

Laravel GraphQL

Latest Stable Version License Tests Downloads Get on Slack

Use Facebook's GraphQL with PHP 7.4+ on Laravel 6.0 & 8.0+. It is based on the PHP port of GraphQL reference implementation. You can find more information about GraphQL in the GraphQL Introduction on the React blog or you can read the GraphQL specifications.

  • Allows creating queries and mutations as request endpoints
  • Supports multiple schemas
    • per schema queries/mutations/types
    • per schema HTTP middlewares
    • per schema GraphQL execution middlewares
  • Custom GraphQL resolver middleware can be defined for each query/mutation

When using the SelectFields class for Eloquent support, additional features are available:

  • Queries return types, which can have custom privacy settings.
  • The queried fields will have the option to be retrieved dynamically from the database.

It offers following features and improvements over the original package by Folklore:

  • Per-operation authorization
  • Per-field callback defining its visibility (e.g. hiding from unauthenticated users)
  • SelectFields abstraction available in resolve(), allowing for advanced eager loading and thus dealing with n+1 problems
  • Pagination support
  • Server-side support for query batching
  • Support for file uploads

Installation

Dependencies:

Installation:

Require the package via Composer:

composer require rebing/graphql-laravel

Laravel

Publish the configuration file:

php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"

Review the configuration file:

config/graphql.php

The default GraphiQL view makes use of the global csrf_token() helper function.

Usage

Concepts

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.

See Resolver middleware for more details.

Schemas

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.

'schemas' => [
    'default' => DefaultSchema::class
]
namespace App\GraphQL\Schemas;

use Rebing\GraphQL\Support\Contracts\ConfigConvertible;

class DefaultSchema implements ConfigConvertible
{
    public function toConfig(): array
    {
        return [
            'query' => [
                ExampleQuery::class,
            ],
            'mutation' => [
                ExampleMutation::class,
            ],
            'types' => [
            
            ],
        ];
    }
}

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

namespace App\GraphQL\Types;

use App\User;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Type as GraphQLType;

class UserType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'User',
        'description'   => 'A user',
        // Note: only necessary if you use `SelectFields`
        'model'         => User::class,
    ];

    public function fields(): 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 here
                    return 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()
    protected function resolveEmailField($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):

'schemas' => [
    'default' => [
        // ...
        
        'types' => [
            App\GraphQL\Types\UserType::class,
        ],

Alternatively you can:

  • add the type on the "global" level, e.g. directly in the root config:

    'types' => [
        App\GraphQL\Types\UserType::class,
    ],

    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.

    GraphQL::addType(\App\GraphQL\Types\UserType::class);

As with queries/mutations, you can use an alias name (though again this prevents it from taking advantage of lazy type loading):

'schemas' => [
    'default' => [
        // ...
        
        'types' => [
            'Useralias' => App\GraphQL\Types\UserType::class,
        ],

Then you need to define a query that returns this type (or a list). You can also specify arguments that you can use in the resolve method.

namespace App\GraphQL\Queries;

use Closure;
use App\User;
use Rebing\GraphQL\Support\Facades\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Query;

class UsersQuery extends Query
{
    protected $attributes = [
        'name' => 'users',
    ];

    public function type(): Type
    {
        return Type::nonNull(Type::listOf(Type::nonNull(GraphQL::type('User'))));
    }

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id', 
                'type' => Type::string(),
            ],
            'email' => [
                'name' => 'email', 
                'type' => Type::string(),
            ]
        ];
    }

    public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        if (isset($args['id'])) {
            return User::where('id' , $args['id'])->get();
        }

        if (isset($args['email'])) {
            return User::where('email', $args['email'])->get();
        }

        return User::all();
    }
}

Add the query to the config/graphql.php configuration file

'schemas' => [
    'default' => [
        'query' => [
            App\GraphQL\Queries\UsersQuery::class
        ],
        // ...
    ]
]

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

query FetchUsers {
    users {
        id
        email
    }
}

For example, if you use homestead:

http://homestead.app/graphql?query=query+FetchUsers{users{id,email}}

Creating a mutation

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:

namespace App\GraphQL\Mutations;

use Closure;
use App\User;
use GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use Rebing\GraphQL\Support\Mutation;

class UpdateUserPasswordMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserPassword'
    ];

    public function type(): Type
    {
        return Type::nonNull(GraphQL::type('User'));
    }

    public function args(): array
    {
        return [
            'id' => [
                'name' => 'id', 
                'type' => Type::nonNull(Type::string()),
            ],
            'password' => [
                'name' => 'password', 
                'type' => Type::nonNull(Type::string()),
            ]
        ];
    }

    public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $user = User::find($args['id']);
        if(!$user) {
            return null;
        }

        $user->password = bcrypt($args['password']);
        $user->save();

        return $user;
    }
}

As you can see in the resolve() method, you use the arguments to update your model and return it.

You should then add the mutation to the config/graphql.php configuration file:

'schemas' => [
    'default' => [
        'mutation' => [
            App\GraphQL\Mutations\UpdateUserPasswordMutation::class,
        ],
        // ...
    ]
]

You can then use the following query on your endpoint to do the mutation:

mutation users {
    updateUserPassword(id: "1", password: "newpassword") {
        id
        email
    }
}

if you use homestead:

http://homestead.app/graphql?query=mutation+users{updateUserPassword(id: "1", password: "newpassword"){id,email}}

File uploads

This library uses https://github.com/laragraph/utils which is compliant with the spec at https://github.com/jaydenseric/graphql-multipart-request-spec .

You have to add the \Rebing\GraphQL\Support\UploadType first to your config/graphql schema types definition (either global or in your schema):

'types' => [
    \Rebing\GraphQL\Support\UploadType::class,
],

It is important that you send the request as multipart/form-data:

WARNING: when you are uploading files, Laravel will use FormRequest - it means that middlewares which are changing request, will not have any effect.

namespace App\GraphQL\Mutations;

use Closure;
use GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;

class UserProfilePhotoMutation extends Mutation
{
    protected $attributes = [
        'name' => 'userProfilePhoto',
    ];

    public function type(): Type
    {
        return GraphQL::type('User');
    }

    public function args(): array
    {
        return [
            'profilePicture' => [
                'name' => 'profilePicture',
                'type' => GraphQL::type('Upload'),
                'rules' => ['required', 'image', 'max:1500'
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap