本文概述
如果你还不熟悉它, GraphQL是一种用于与你的API交互的查询语言, 与REST等替代架构相比, 它提供了一些好处。 GraphQL用作移动和单页应用程序的端点时非常方便。 GraphQL允许你相对轻松地查询请求中的嵌套数据和相关数据, 从而使开发人员可以在一次往返服务器的过程中获得所需的确切数据。
Laravel是一个流行的, 自以为是的PHP Web框架。它提供了许多内置工具来使应用程序快速启动和运行, 但也允许开发人员在需要时将自己的实现换成Laravel的内置接口。
尽管自从开源以来, 围绕GraphQL和Laravel的社区已经有了飞跃的发展, 但是解释如何一起使用这两种技术的文档仍然很少。
因此, 在本教程中, 我将向你展示如何使用Laravel创建自己的GraphQL服务器。
项目概况
在开始之前, 我们需要熟悉我们尝试构建的项目。为此, 我们将定义资源并创建GraphQL模式, 稍后将其用于提供API。
项目资源
我们的应用程序将包含两个资源:文章和用户。这些资源将在我们的GraphQL模式中定义为对象类型:
type User {
id: ID!
name: String!
email: String!
articles: [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
author: User!
}
查看模式, 我们可以看到我们两个对象之间存在一对多关系。用户可以写很多文章, 并且为文章分配了作者(用户)。
现在我们已经定义了对象类型, 我们需要一种创建和查询数据的方法, 因此让我们定义查询和突变对象:
type Query {
user(id: ID!): User
users: [User!]!
article(id: ID!): Article
articles: [Article!]!
}
type Mutation {
createUser(name: String!, email: String!, password: String!): User
createArticle(title: String!, content: String!): Article
}
设置我们的Laravel项目
现在我们已经定义了GraphQL模式, 让我们启动并运行Laravel项目。首先, 通过Composer项目创建一个新的Laravel:
$ composer create-project --prefer-dist laravel/laravel laravel-graphql
为了确保我们一切正常, 让我们启动服务器并确保看到Laravel的默认页面:
$ cd laravel-graphql
$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>
数据库模型和迁移
就本文而言, 我们将使用SQLite。因此, 我们对默认的.env文件进行以下更改:
DB_CONNECTION=sqlite
# DB_HOST=
# DB_PORT=
# DB_DATABASE=database.sqlite
# DB_USERNAME=
# DB_PASSWORD=
接下来, 让我们创建数据库文件:
$ touch ./database/database.sqlite
Laravel附带了一个用户模型和一些基本的迁移文件。让我们在Laravel提供给我们的CreateUsersTable迁移文件中快速向我们添加api_token列:
/database/migrations/XXXX_XX_XX_000000_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('api_token', 80)->unique()->nullable()->default(null);
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::dropIfExists('users');
}
}
在获得授权后, 我们将在本文后面的内容中转回到该附加列。现在开始创建文章模型和迁移文件以创建关联的表:
$ php artisan make:model Article -m
注意:-m选项为我们新创建的文章模型创建一个迁移文件。
让我们对生成的迁移文件进行一些调整:
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('title');
$table->text('content');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
我们添加了一个外键, 指向用户表上的ID以及在GraphQL模式中定义的title和content列。
现在我们已经定义了迁移文件, 让我们继续对数据库运行它们:
$ php artisan migrate
接下来, 让我们通过定义必要的关系来更新模型:
app/User.php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password', ];
// ...
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function articles()
{
return $this->hasMany(Article::class);
}
}
app/Article.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title', 'content', ];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
数据库播种
现在我们已经建立了模型和迁移, 让我们播种数据库。首先, 为我们的article和users表创建一些种子类:
$ php artisan make:seeder UsersTableSeeder
$ php artisan make:seeder ArticlesTableSeeder
接下来, 让我们对其进行设置, 以将一些虚拟数据插入我们的SQLite数据库:
database/seeds/UsersTableSeeder.php
use App\User;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
\App\User::truncate();
$faker = \Faker\Factory::create();
$password = bcrypt('secret');
\App\User::create([
'name' => $faker->name, 'email' => '[email protected]', 'password' => $password, ]);
for ($i = 0; $i < 10; ++$i) {
\App\User::create([
'name' => $faker->name, 'email' => $faker->email, 'password' => $password, ]);
}
}
}
database/seeds/ArticlesTableSeeder.php
use App\Article;
use Illuminate\Database\Seeder;
class ArticlesTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
\App\Article::truncate();
\App\Article::unguard();
$faker = \Faker\Factory::create();
\App\User::all()->each(function ($user) use ($faker) {
foreach (range(1, 5) as $i) {
\App\Article::create([
'user_id' => $user->id, 'title' => $faker->sentence, 'content' => $faker->paragraphs(3, true), ]);
}
});
}
}
/database/seeds/DatabaseSeeder.php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(UsersTableSeeder::class);
$this->call(ArticlesTableSeeder::class);
}
}
最后, 让我们继续运行数据库播种器, 以将一些数据放入我们的数据库中:
$ php artisan db:seed
Laravel Lighthouse和GraphQL服务器
现在我们已经建立了数据库和模型, 是时候开始构建GraphQL服务器了。目前, 有几种可用于Laravel的解决方案, 但是在本文中, 我们将使用Lighthouse。
Lighthouse是我几年前创建的一个程序包, 最近得到了周围不断增长的社区的一些惊人支持。它使开发人员可以使用使用Laravel的样板快速设置GraphQL服务器, 同时还具有足够的灵活性以允许开发人员对其进行自定义以满足几乎所有项目的需求。
首先, 将包拉入我们的项目:
$ composer require nuwave/lighthouse:"3.1.*"
接下来, 让我们发布Lighthouse的配置文件:
$ php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=config
注意:你还可以选择删除–tag = config选项, 以选择发布Lighthouse的默认架构文件。但是出于本文的目的, 我们将从头开始创建架构文件。
如果我们看一下config / lighthouse.php文件, 你会注意到一个用于在Lighthouse中注册我们的模式文件的设置:
'schema' => [
'register' => base_path('graphql/schema.graphql'), ],
因此, 让我们继续创建我们的架构文件, 并设置我们的用户对象类型和查询:
$ mkdir graphql
$ touch ./graphql/schema.graphql
/graphql/schema.graphql
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID! @eq): User @find
users: [User!]! @all
}
你会注意到, 我们的架构与我们之前定义的架构相似, 只是我们添加了一些称为架构指令的标识符。
让我们花一点时间来分解我们定义的架构。我们的第一个定义是一个名为User的对象类型, 它与App \ User雄辩的模型有关系。我们将ID, 名称和电子邮件定义为可从用户模型中查询的字段。另外, 这意味着密码, created_at和updated_at列是无法从我们的API查询的字段。
接下来, 我们有查询类型, 它是API的入口点, 可用于查询数据。我们的第一个字段是users字段, 它返回User对象类型的数组。 @all指令告诉Lighthouse使用我们的用户模型运行Eloquent查询并获取所有结果。这与运行以下命令相同:
$users = \App\User::all();
注意:Lighthouse知道在\ App \ User命名空间中寻找模型, 因为在其配置文件中定义了namespaces选项。
我们在查询类型上定义的第二个字段是调用用户, 它以id作为参数并返回一个User对象类型。我们还添加了两个指令, 以帮助Lighthouse自动为我们建立查询并返回单个User模型。 @eq指令指示Lighthouse在我们的id列上添加一个位置, 而@find指令指示Lighthouse返回单个结果。要使用Laravel的查询构建器来编写此查询, 它看起来应该像这样:
$user = \App\User::where('id', $args['id'])->first();
查询我们的GraphQL API
现在, 我们对Lighthouse如何使用我们的架构创建查询有了一些了解, 让我们运行服务器并开始查询数据。我们将从运行服务器开始:
$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>
要查询GraphQL终结点, 可以在终端或标准客户端(如Postman)中运行cURL命令。但是, 为了获得GraphQL的全部好处(例如自动完成, 错误突出显示, 文档等), 我们将使用GraphQL Playground(此处提供下载版本)。
启动Playground时, 单击” URL端点”选项卡, 然后输入http:// localhost:8000 / graphql将GraphQL Playground指向我们的服务器。在编辑器的左侧, 我们可以查询我们的数据, 因此, 首先请询问我们作为数据库种子的所有用户:
{
users {
id
email
name
}
}
当你点击IDE中间的播放按钮(或单击Ctrl + Enter)时, 你会在右侧看到服务器的JSON输出, 如下所示:
{
"data": {
"users": [
{
"id": "1", "email": "[email protected]", "name": "Carolyn Powlowski"
}, {
"id": "2", "email": "[email protected]", "name": "Elouise Raynor"
}, {
"id": "3", "email": "[email protected]", "name": "Mrs. Dejah Wiza"
}, ...
]
}
}
注意:因为我们使用Faker来播种数据库, 所以email和name字段中的数据将不同。
现在, 让我们尝试查询一个用户:
{
user(id: 1) {
email
name
}
}
我们将为单个用户获得以下输出:
{
"data": {
"user": {
"email": "[email protected]", "name": "Carolyn Powlowski"
}
}
}
像这样查询数据很容易上手, 但是极不可能出现在你想查询所有数据的项目中, 因此, 我们尝试添加一些分页。在查看Lighthouse的各种内置指令时, 我们有一个@paginate指令可供我们使用, 因此让我们像这样更新架构的查询对象:
type Query {
user(id: ID! @eq): User @find
users: [User!]! @paginate
}
如果我们重新加载GraphQL Playground(Ctrl / Cmd + R)并再次尝试查询用户, 你会注意到我们收到一条错误消息, 指出无法查询类型” UserPaginator”的字段” id”, 那么发生了什么?在幕后, Lighthouse为我们操纵了模式以获取一组分页的结果, 并通过更改用户字段的返回类型来做到这一点。
让我们仔细检查一下GraphQL Playground的”文档”标签中的架构。如果你查看users字段, 它将返回一个UserPaginator, 它返回一个用户数组和一个Lighthouse定义的PaginatorInfo类型:
type UserPaginator {
paginatorInfo: PaginatorInfo!
data: [User!]!
}
type PaginatorInfo {
count: Int!
currentPage: Int!
firstItem: Int
hasMorePages: Boolean!
lastItem: Int
lastPage: Int!
perPage: Int!
total: Int!
}
如果你熟悉Laravel的内置分页, 那么你对PaginatorInfo类型中可用的字段可能会非常熟悉。因此, 要查询两个用户, 获取系统中的用户总数, 并检查是否有更多页面需要循环浏览, 我们将发送以下查询:
{
users(count:2) {
paginatorInfo {
total
hasMorePages
}
data {
id
name
email
}
}
}
这将为我们提供以下响应:
{
"data": {
"users": {
"paginatorInfo": {
"total": 11, "hasMorePages": true
}, "data": [
{
"id": "1", "name": "Carolyn Powlowski", "email": "[email protected]"
}, {
"id": "2", "name": "Elouise Raynor", "email": "[email protected]"
}, ]
}
}
}
人际关系
通常, 在开发应用程序时, 你的许多数据都是相关的。在我们的情况下, 用户可以写很多文章, 因此让我们将该关系添加到我们的用户类型并定义我们的文章类型:
type User {
id: ID!
name: String!
email: String!
articles: [Article!]! @hasMany
}
type Article {
id: ID!
title: String!
content: String!
}
在这里, 我们使用另一个Lighthouse提供的架构指令@hasMany, 它告诉Lighthouse我们的用户模型与Article模型具有\ Illuminate \ Database \ Eloquent \ Relations \ HasMany关系。
现在, 让我们查询一下我们新定义的关系:
{
user(id:1) {
articles {
id
title
}
}
}
这将为我们提供以下响应:
{
"data": {
"user": {
"articles": [
{
"id": "1", "title": "Aut velit et temporibus ut et tempora sint."
}, {
"id": "2", "title": "Voluptatem sed labore ea voluptas."
}, {
"id": "3", "title": "Beatae sit et maxime consequatur et natus totam."
}, {
"id": "4", "title": "Corrupti beatae cumque accusamus."
}, {
"id": "5", "title": "Aperiam quidem sit esse rem sed cupiditate."
}
]
}
}
}
最后, 让我们反转关系, 并使用Lighthouse的@belongsTo模式指令将作者关系添加到Article对象类型中, 并更新查询:
type Article {
id: ID!
title: String!
content: String!
author: User! @belongsTo(relation: "user")
}
type Query {
user(id: ID! @eq): User @find
users: [User!]! @paginate
article(id: ID! @eq): Article @find
articles: [Article!]! @paginate
}
你会看到我们在@belongsTo指令中添加了一个可选的关系参数。这就告诉Lighthouse使用Articles模型的用户关系, 并将其分配给author字段。
现在, 让我们查询文章列表并获取其相关作者:
{
articles(count:2) {
paginatorInfo {
total
hasMorePages
}
data {
id
title
author {
name
email
}
}
}
}
而且我们应该从我们的服务器中获取以下信息:
{
"data": {
"articles": {
"paginatorInfo": {
"total": 55, "hasMorePages": true
}, "data": [
{
"id": "1", "title": "Aut velit et temporibus ut et tempora sint.", "author": {
"name": "Carolyn Powlowski", "email": "[email protected]"
}
}, {
"id": "2", "title": "Voluptatem sed labore ea voluptas.", "author": {
"name": "Carolyn Powlowski", "email": "[email protected]"
}
}
]
}
}
}
GraphQL突变
现在我们可以查询数据了, 让我们创建一些变体来创建新的用户和文章。我们将从用户模型开始:
type Mutation {
createUser(
name: String!
email: String! @rules(apply: ["email", "unique:users"])
password: String! @bcrypt @rules(apply: ["min:6"])
): User @create
}
现在, 让我们分解一下该架构定义。我们创建了一个名为createUser的变体, 它带有三个参数(名称, 电子邮件和密码)。我们已将@rules指令应用于电子邮件和密码参数。这可能看起来有点熟悉, 因为它类似于Laravel为控制器提供的验证逻辑。
接下来, 我们将@bcrypt指令附加到我们的密码字段。这将在将密码传递给新创建的模型之前对其进行加密。
最后, 为帮助我们创建新模型, Lighthouse提供了@create schema指令, 该指令将采用我们定义的参数并创建一个新模型。在Controller中执行相同的逻辑如下所示:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Create a new user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$data = $this->validate($request, [
'email' => ['email', 'unique:users'], 'password' => ['min:6']
]);
$user = \App\User::create($data);
return response()->json(['user' => $user]);
}
}
现在我们已经设置好createUser突变字段, 让我们继续在GraphQL Playground中运行以下代码:
mutation {
createUser(
name:"John Doe"
email:"[email protected]"
password: "secret"
) {
id
name
email
}
}
我们应该得到以下输出:
{
"data": {
"createUser": {
"id": "12", "name": "John Doe", "email": "[email protected]"
}
}
}
GraphQL身份验证和授权
由于我们需要在我们的Article模型中添加一个user_id, 因此现在是在GraphQL / Lighthouse中进行身份验证和授权的好时机。
要验证用户身份, 我们需要向他们提供api_token, 因此让我们创建一个突变来处理该问题, 然后添加@field指令以将Lighthouse指向将处理逻辑的自定义解析器。我们使用resolver参数将解析器设置为与在Laravel中定义控制器相同的模式。
使用下面定义的@field指令, 我们告诉Lighthouse运行登录突变的时间, 请在App \ GraphQL \ Mutations \ AuthMutator类上使用createToken方法:
type Mutation {
# ...
login(
email: String!
password: String!
): String @field(resolver: "[email protected]")
}
注意:你无需在此处包括整个名称空间。在lighthouse.php配置文件中, 你将看到我们已经为突变定义的名称空间已设置为App \\ GraphQL \\ Mutations-但是, 你可以根据需要使用完整的名称空间。
让我们使用Lighthouse的生成器来创建新的mutator类:
$ php artisan lighthouse:mutation AuthMutator
接下来, 让我们更新解析器功能, 如下所示:
namespace App\GraphQL\Mutations;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class AuthMutator
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param mixed[] $args The arguments that were passed into the field.
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context Arbitrary data that is shared between all fields of a single query.
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
* @return mixed
*/
public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
$credentials = Arr::only($args, ['email', 'password']);
if (Auth::once($credentials)) {
$token = Str::random(60);
$user = auth()->user();
$user->api_token = $token;
$user->save();
return $token;
}
return null;
}
}
现在我们已经设置好解析器, 让我们对其进行测试, 并尝试使用GraphQL Playground中的以下变体获取API令牌:
mutation {
login(email:"[email protected]", password:"secret")
}
我们应该像这样将令牌发送回给我们:
{
"data": {
"login": "VJCz1DCpmdvB9WatqvWbXBP2RN8geZQlrQatUnWIBJCdbAyTl3UsdOuio3VE"
}
}
注意:请确保复制登录突变返回的令牌, 以便我们稍后使用。
接下来, 让我们添加一个查询字段, 该字段将返回经过身份验证的用户, 以确保我们的逻辑有效。我们将添加一个名为我的字段, 并使用Lighthouse的@auth指令返回当前已通过身份验证的用户。我们还将将guard参数设置为api, 因为这是我们将对用户进行身份验证的方式。
type Query {
# ...
me: User @auth(guard: "api")
}
现在运行查询。在GraphQL Playground中, 你可以通过双击底部的” Http Headers”选项卡来设置请求标头。我们添加带有JSON对象的标头, 因此要向每个请求添加承载令牌, 你需要添加以下内容:
{
"Authorization": "Bearer VJCz1DCpmdvB9WatqvWbXBP2RN8geZQlrQatUnWIBJCdbAyTl3UsdOuio3VE"
}
注意:将承载令牌替换为你在运行登录查询时收到的令牌。
现在, 我们运行me查询:
{
me {
email
articles {
id
title
}
}
}
我们应该得到如下所示的输出:
{
"data": {
"me": {
"email": "[email protected]", "articles": [
{
"id": "1", "title": "Rerum perspiciatis et quos occaecati exercitationem."
}, {
"id": "2", "title": "Placeat quia cumque laudantium optio voluptatem sed qui."
}, {
"id": "3", "title": "Optio voluptatem et itaque sit animi."
}, {
"id": "4", "title": "Excepturi in ad qui dolor ad perspiciatis adipisci."
}, {
"id": "5", "title": "Qui nemo blanditiis sed fugit consequatur."
}
]
}
}
}
中间件
现在我们知道我们的身份验证工作正常, 让我们创建最后一个变体, 以使用当前身份验证的用户创建文章。我们将使用@field指令将Lighthouse指向我们的解析器, 并且还将包括@middleware指令以确保用户已登录。
type Mutation {
# ...
createArticle(title: String!, content: String!): Article
@field(resolver: "[email protected]")
@middleware(checks: ["auth:api"])
}
首先, 让我们生成一个突变类:
$ php artisan lighthouse:mutation ArticleMutator
接下来, 让我们使用以下逻辑更新mutator:
namespace App\GraphQL\Mutations;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class ArticleMutator
{
/**
* Return a value for the field.
*
* @param null $rootValue
* @param mixed[] $args
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context
* @return mixed
*/
public function create($rootValue, array $args, GraphQLContext $context)
{
$article = new \App\Article($args);
$context->user()->articles()->save($article);
return $article;
}
}
注意:我们重命名了要创建的默认解析功能。你无需为每个解析器创建一个新的类。相反, 如果更合理, 则可以将逻辑分组在一起。
最后, 让我们运行新的变体并检查输出。确保在” HTTP标头”标签中保留上一个查询中的”授权”标头:
mutation {
createArticle(
title:"Building a GraphQL Server with Laravel"
content:"In case you're not currently familiar with it, GraphQL is a query language used to interact with your API..."
) {
id
author {
id
email
}
}
}
我们应该得到以下输出:
{
"data": {
"createArticle": {
"id": "56", "author": {
"id": "1", "email": "[email protected]"
}
}
}
}
本文总结
回顾一下, 我们利用Lighthouse为我们的Laravel项目创建了GraphQL服务器。我们利用了一些内置的架构指令, 创建了查询和变异, 并处理了授权和身份验证。
Lighthouse使你可以做更多的事情(例如, 允许你创建自己的自定义架构指令), 但是出于本文的目的, 我们坚持了基础知识, 并且能够以很少的样板启动并运行GraphQL服务器。
下次需要为移动或单页应用程序设置API时, 请务必考虑使用GraphQL作为查询数据的方法!
评论前必须登录!
注册