使用 Laravel 构建 GraphQL 服务器
已发表: 2022-03-11如果您仍然不熟悉它,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'); } }
我们添加了一个外键,指向users
表上的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); } }
数据库播种
现在我们已经建立了模型和迁移,让我们为我们的数据库播种。 我们将从为我们的articles
和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
注意:您也可以选择发布 Lighthouse 的默认架构文件,只需删除--tag=config
选项即可。 但是出于本文的目的,我们将从头开始创建我们的模式文件。
如果我们看一下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
、 name
和email
定义为可以从我们的User
模型中查询的字段。 或者,这意味着password
、 created_at
和updated_at
列是无法从我们的 API 查询的字段。
接下来我们有我们的Query
类型,它是我们 API 的入口点,可用于查询数据。 我们的第一个字段是返回User
对象类型数组的users
字段。 @all
指令告诉 Lighthouse 使用我们的User
模型运行 Eloquent 查询并获取所有结果。 这与运行以下命令相同:
$users = \App\User::all();
注意: Lighthouse 知道在\App\User
命名空间中查找模型,因为它的配置文件中定义了namespaces
选项。
我们在查询类型上定义的第二个字段是调用user
,它以id
作为参数并返回单个User
对象类型。 我们还添加了两个指令来帮助 Lighthouse 自动为我们构建查询并返回单个User
模型。 @eq
指令告诉 Lighthouse 在我们的id
列上添加 where, @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 Endpoint”选项卡,然后输入 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 ) 并再次尝试我们的users
查询,您会注意到我们收到一条错误消息,指出Cannot query field "id" on type "UserPaginator"
,那么发生了什么? 在幕后,Lighthouse 为我们操纵我们的模式以获得一组分页的结果,并通过更改users
字段的返回类型来实现。
让我们通过在 GraphQL Playground 的“Docs”选项卡中检查我们的模式来仔细看看。 如果你看一下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]" }, ] } } }
关系
通常,在开发应用程序时,您的大部分数据都是相关的。 在我们的例子中,一个User
可以写很多Articles
,所以让我们将该关系添加到我们的 User 类型并定义我们的Article
类型:
type User { id: ID! name: String! email: String! articles: [Article!]! @hasMany } type Article { id: ID! title: String! content: String! }
在这里,我们使用另一个 Lighthouse 提供的模式指令@hasMany
,它告诉 Lighthouse 我们的User
模型与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
模式指令以及更新我们的Query
将我们的author
关系添加到我们的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
指令中添加了一个可选的relation
参数。 这告诉 Lighthouse 使用Articles
模型的user
关系并将其分配给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
的突变,它接受三个参数( name
、 email
和password
)。 我们已将@rules
指令应用于我们的email
和password
参数。 这可能看起来有点熟悉,因为它类似于 Laravel 为其控制器提供的验证逻辑。
接下来,我们将@bcrypt
指令附加到我们的password
字段。 这将在将密码传递给新创建的模型之前对其进行加密。
最后,为了帮助我们创建新模型,Lighthouse 提供了一个@create
模式指令,它将采用我们定义的参数并创建一个新模型。 在 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 指向一个将处理逻辑的自定义解析器。 我们以与在 Laravel 中使用resolver
参数定义控制器相同的模式设置解析器。
使用下面定义的@field
指令,我们告诉 Lighthouse 当login
变更运行时,在我们的App\GraphQL\Mutations\AuthMutator
类上使用createToken
方法:
type Mutation { # ... login( email: String! password: String! ): String @field(resolver: "AuthMutator@resolve") }
注意:您不需要在此处包含整个命名空间。 在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" } }
注意:请务必复制从登录突变返回的令牌,以便我们以后使用它。
接下来,让我们添加一个查询字段,该字段将返回经过身份验证的用户,以确保我们的逻辑正常工作。 我们将添加一个名为me
的字段并使用 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: "ArticleMutator@create") @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; } }
注意:我们将默认resolve
函数重命名为create
。 您不需要为每个解析器创建一个新类。 相反,如果更有意义,您可以将逻辑组合在一起。
最后,让我们运行我们的新突变并检查输出。 请务必保留我们之前在“HTTP Headers”选项卡中查询的Authorization
标头:
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 作为查询数据的一种方式!