使用 Laravel 構建 GraphQL 服務器

已發表: 2022-03-11

如果您仍然不熟悉它,GraphQL 是一種用於與您的 API 交互的查詢語言,與 REST 等替代架構相比,它提供了一些優勢。 GraphQL 在用作移動和單頁應用程序的端點時非常方便。 GraphQL 允許您相對輕鬆地查詢請求中的嵌套和相關數據,允許開發人員在與服務器的單次往返中獲得他們需要的準確數據。

Laravel 是一個流行的、自以為是的 PHP Web 框架。 它提供了許多內置工具來快速啟動和運行應用程序,但它也允許開發人員在需要時將自己的實現換成 Laravel 的內置接口。

儘管圍繞 GraphQL 和 Laravel 的社區自開源以來發展迅速,但解釋如何一起使用這兩種技術的文檔仍然有些稀缺。

因此,在本教程中,我將向您展示如何使用 Laravel 創建自己的 GraphQL 服務器。

項目概況

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 模式中定義的titlecontent列。

現在我們已經定義了遷移文件,讓我們繼續對我們的數據庫運行它們:

 $ 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); } }

數據庫播種

現在我們已經建立了模型和遷移,讓我們為我們的數據庫播種。 我們將從為我們的articlesusers表創建一些播種器類開始:

 $ 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 服務器,幾乎沒有樣板文件,同時也足夠靈活,允許開發人員自定義它以滿足幾乎任何項目的需求。

Laravel Lighthouse 和 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雄辯模型有關。 我們將idnameemail定義為可以從我們的User模型中查詢的字段。 或者,這意味著passwordcreated_atupdated_at列是無法從我們的 API 查詢的字段。

接下來我們有我們的Query類型,它是我們 API 的入口點,可用於查詢數據。 我們的第一個字段是返回users對像類型數組的User字段。 @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 播種我們的數據庫,所以emailname字段中的數據會有所不同。

現在讓我們嘗試查詢單個用戶:

 { 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的突變,它接受三個參數( nameemailpassword )。 我們已將@rules指令應用於我們的emailpassword參數。 這可能看起來有點熟悉,因為它類似於 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 作為查詢數據的一種方式!

相關:完整的用戶身份驗證和訪問控制 - Laravel Passport 教程,Pt。 1