如何在Laravel工程里搭建一个GraphQL服务

什么是GraphQL?

GraphQL.Restful风格的API设计在面对前端日渐复杂的交互需求中显得过于死板,缺乏足够的灵活性。所以Facebook为了满足自身业务需求的变化, 舍弃Restful风格的API设计,而自己研发的一套查询协议和实现,就是GraphQL.

它的设计理念

个人认为GraphQL是数据库查询语言sql的设计思想在API开发领域的延展.就像定义一个sql查询一样,我们在客户端定义一组查询语言即可获取到自己 想要的一定结构的数据。

GraphQL并不是一个面向图数据库的查询语言,而是一个数据抽象层,包括数据格式、数据关联、查询方式定义与实现等等一揽子的东西。 GraphQL也并不是一个具体的后端编程框架,如果将REST看做适合于简单逻辑的查询标准,那么GraphQL可以做一个独立的抽象层, 通过对于多个REST风格的简单的接口的排列组合提供更多复杂多变的查询方式。与REST相比,GraphQL定义了更严格、可扩展、可维护的数据查询方式。

基于laravel的服务端实现

这个工程已经完整的实现了GraphQL.所以我们在这里就直接使用 它来。

首先我们在一个laravel的工程里面安装这个包

composer require “folklore/graphql”

然后我们将它的ServiceProvider加入到config/app.php的配置里面

1- Add the service provider to your app/config/app.php file

Folklore\GraphQL\ServiceProvider::class,

2- Add the facade to your app/config/app.php file

'GraphQL' => Folklore\GraphQL\Support\Facades\GraphQL::class,

3- Publish the configuration file

$ php artisan vendor:publish --provider="Folklore\GraphQL\ServiceProvider"

上面的Publish命令生成了一个配置文件config/graphql.php,我们看看

<?php


return [

    /*
     * The prefix for routes
     */
    'prefix' => 'graphql',

    /*
     * The routes to make GraphQL request. Either a string that will apply
     * to both query and mutation or an array containing the key 'query' and/or
     * 'mutation' with the according Route
     *
     * Example:
     *
     * Same route for both query and mutation
     *
     * 'routes' => [
     *     'query' => 'query/{graphql_schema?}',
     *     'mutation' => 'mutation/{graphql_schema?}',
     *      mutation' => 'graphiql'
     * ]
     *
     * you can also disable routes by setting routes to null
     *
     * 'routes' => null,
     */
    'routes' => '{graphql_schema?}',

    /*
     * The controller to use in GraphQL requests. Either a string that will apply
     * to both query and mutation or an array containing the key 'query' and/or
     * 'mutation' with the according Controller and method
     *
     * Example:
     *
     * 'controllers' => [
     *     'query' => '\Folklore\GraphQL\GraphQLController@query',
     *     'mutation' => '\Folklore\GraphQL\GraphQLController@mutation'
     * ]
     */
    'controllers' => \Folklore\GraphQL\GraphQLController::class.'@query',

    /*
     * The name of the input variable that contain variables when you query the
     * endpoint. Most libraries use "variables", you can change it here in case you need it.
     * In previous versions, the default used to be "params"
     */
    'variables_input_name' => 'variables',

    /*
     * Any middleware for the 'graphql' route group
     */
    'middleware' => [],

    /*
     * Any headers that will be added to the response returned by the default controller
     */
    'headers' => [],

    /*
     * Any JSON encoding options when returning a response from the default controller
     * See http://php.net/manual/function.json-encode.php for the full list of options
     */
    'json_encoding_options' => 0,

    /*
     * Config for GraphiQL (see (https://github.com/graphql/graphiql).
     * To dissable GraphiQL, set this to null
     */
    'graphiql' => [
        'routes' => '/graphiql/{graphql_schema?}',
        'controller' => \Folklore\GraphQL\GraphQLController::class.'@graphiql',
        'middleware' => [],
        'view' => 'graphql::graphiql'
    ],

    /*
     * The name of the default schema used when no arguments are provided
     * to GraphQL::schema() or when the route is used without the graphql_schema
     * parameter
     */
    'schema' => 'default',

    /*
     * The schemas for query and/or mutation. It expects an array to provide
     * both the 'query' fields and the 'mutation' fields. You can also
     * provide an GraphQL\Schema object directly.
     *
     * Example:
     *
     * 'schemas' => [
     *     'default' => new Schema($config)
     * ]
     *
     * or
     *
     * 'schemas' => [
     *     'default' => [
     *         'query' => [
     *              'users' => 'App\GraphQL\Query\UsersQuery'
     *          ],
     *          'mutation' => [
     *
     *          ]
     *     ]
     * ]
     */
    'schemas' => [
        'default' => [
            'query' => [
                'users' => 'App\GraphQL\Query\UsersQuery'
            ],
            'mutation' => [
              'updateUserPassword' => 'App\GraphQL\Mutation\UpdateUserPasswordMutation'
            ]
        ]
    ],

    /*
     * The types available in the application. You can access them from the
     * facade like this: GraphQL::type('user')
     *
     * Example:
     *
     * 'types' => [
     *     'user' => 'App\GraphQL\Type\UserType'
     * ]
     *
     * or without specifying a key (it will use the ->name property of your type)
     *
     * 'types' =>
     *     'App\GraphQL\Type\UserType'
     * ]
     */
    'types' => [
        'User' => 'App\GraphQL\Type\UserType'
    ],

    /*
     * This callable will receive all the Exception objects that are caught by GraphQL.
     * The method should return an array representing the error.
     *
     * Typically:
     *
     * [
     *     'message' => '',
     *     'locations' => []
     * ]
     */
    'error_formatter' => [\Folklore\GraphQL\GraphQL::class, 'formatError'],

    /*
     * Options to limit the query complexity and depth. See the doc
     * @ https://github.com/webonyx/graphql-php#security
     * for details. Disabled by default.
     */
    'security' => [
        'query_max_complexity' => null,
        'query_max_depth' => null,
        'disable_introspection' => false
    ]
];

当我们创建了Type,Query, Mutation后,就需要将他们写入到上面的配置文件中。

创建Type

    namespace App\GraphQL\Type;

    use GraphQL\Type\Definition\Type;
    use Folklore\GraphQL\Support\Type as GraphQLType;

    class UserType extends GraphQLType {

        protected $attributes = [
          'name' => 'User',
          'description' => 'A user'
        ];

        /*
        * Uncomment following line to make the type input object.
        * http://graphql.org/learn/schema/#input-types
        */
        // protected $inputObject = true;

        public function fields()
        {
            return [
              'id' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the user'
              ],
              'email' => [
                'type' => Type::string(),
                'description' => 'The email of user'
              ]
            ];
        }


        // If you want to resolve the field yourself, you can declare a method
        // with the following format resolve[FIELD_NAME]Field()
        protected function resolveEmailField($root, $args)
        {
        return strtolower($root->email);
        }

    }

将创建的Type写入config/graphql.php配置文件

    'types' => [
    	'User' => 'App\GraphQL\Type\UserType'
    ]

创建Query

  namespace App\GraphQL\Query;

  use GraphQL;
  use GraphQL\Type\Definition\Type;
  use Folklore\GraphQL\Support\Query;
  use App\User;

  class UsersQuery extends Query {

      protected $attributes = [
        'name' => 'users'
      ];

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

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

      public function resolve($root, $args)
      {
        if(isset($args['id']))
        {
          return User::where('id' , $args['id'])->get();
        }
        else if(isset($args['email']))
        {
          return User::where('email', $args['email'])->get();
        }
        else
        {
          return User::all();
        }
      }

  }

将这个Query也放入config/graphql.php配置文件中

  'schemas' => [
  	'default' => [
  		'query' => [
  			'users' => 'App\GraphQL\Query\UsersQuery'
  		],
  		// ...
  	]
  ]

这样我们可以开启服务器检查这个查询是否正常运行:

php artisan serve

用浏览器访问: http://localhost:8000/graphql?query=query+FetchUsers{users{id,email}}

输出返回:

{"data":{"users":[{"id":"1","email":"756127792@qq.com"},{"id":"2","email":"zoobile@gmail.com"},{"id":"3","email":"756127793@qq.com"}]}}

创建Mutation

Mutation是一种带有参数的查询,这些参数用来做数据的更新,然后会返回一个特定类型的对象。

例如我们要更新用户的密码,那么我们需要创建一个UpdateUserPasswordMutation:

  namespace App\GraphQL\Mutation;

  use GraphQL;
  use GraphQL\Type\Definition\Type;
  use Folklore\GraphQL\Support\Mutation;
  use App\User;

  class UpdateUserPasswordMutation extends Mutation {

      protected $attributes = [
        'name' => 'updateUserPassword'
      ];

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

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

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

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

          return $user;
      }

  }

可以看到resolve方法用来更新密码并返回更新后的用户实例。

我们需要将Mutation加入到配置文件中:

  'schema' => [
  	'default' => [
  		'mutation' => [
  			'updateUserPassword' => 'App\GraphQL\Mutation\UpdateUserPasswordMutation'
  		],
  		// ...
  	]
  ]

OK,我们可以执行密码更新的操作了:

http://localhost:8000/graphql?query=mutation+users{updateUserPassword(id: “1”, password: “newpassword”){id,email}}

返回:

{"data":{"updateUserPassword":{"id":"1","email":"756127792@qq.com"}}}

相关资源

  1. GraphQL explained
  2. Coursera’s journey to GraphQL:Adding GraphQL to a REST and microservices backend