В этой статье я расскажу вам, как создать свой собственный сервер GraphQL при помощи Laravel. Мы используем Lighthouse и научимся использовать встроенные директивы, создавать запросы и мутации, а также обрабатывать авторизацию и аутентификацию.

GraphQL — это язык запросов для API, который обеспечивает определенные преимущества по сравнению с альтернативными архитектурами, такими как REST. GraphQL чрезвычайно удобен, когда используется в качестве конечной точки для мобильных и одностраничных приложений. Он позволяет с относительной легкостью запрашивать вложенные и связанные данные в запросе, что позволяет разработчикам получать именно те данные, которые им нужны, за одно обращение на сервер.

Хотя сообщества GraphQL и Laravel значительно выросли с момента их появления, но документации, объясняющей, как использовать эти две технологии вместе, все еще мало.

Обзор проекта

Прежде чем начнем, давайте ознакомимся с проектом, который мы будем делать. Мы определим наши ресурсы и создадим схему GraphQL, которую позже будем использовать для обслуживания нашего API.

Ресурсы проекта

Наше приложение будет состоять из двух ресурсов: Articles (Статьи) и Users (Пользователи). Эти ресурсы будут определены как объекты в схеме 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:

composer create-project --prefer-dist laravel/laravel laravel-graphql

Для того, чтобы убедиться, что все работает, давайте загрузим наш сервер и посмотрим на дефолтную страницу:

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 уже поставляется с моделью User и основными файлами миграции. Добавим столбец api_token в миграцию CreateUsersTable:

// /database/migrations/XXXX_XX_XX_000000_create_users_table.php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
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'); }
}

Мы вернемся к этому столбцу позже, когда займемся авторизацией. Продолжим и создадим модель Article и файл миграции для неё:

php artisan make:model Article -m

Примечание. Параметр -m создает файл миграции для создаваемой модели.

Кое-что изменим в миграции:

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

Мы добавили внешний ключ, указывающий на id в таблице users, а также столбцы title и content, которые мы определили в схеме GraphQL.

Теперь, когда у нас есть файлы миграции, давайте их запустим:

php artisan migrate

Обновим наши модели, задав необходимые отношения:

//app/User.php
namespace App;
use IlluminateNotificationsNotifiable;
use IlluminateFoundationAuthUser as Authenticatable;
class User extends Authenticatable
{ use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; // ... /** * @return IlluminateDatabaseEloquentRelationsHasMany */ public function articles() { return $this->hasMany(Article::class); }
}
//app/Article.php
namespace App;
use IlluminateDatabaseEloquentModel;
class Article extends Model
{ /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'title', 'content', ]; /** * @return IlluminateDatabaseEloquentRelationsBelongsTo */ 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 AppUser;
use IlluminateDatabaseSeeder;
class UsersTableSeeder extends Seeder
{ /** * Run the database seeds. */ public function run() { AppUser::truncate(); $faker = FakerFactory::create(); $password = bcrypt('secret'); AppUser::create([ 'name' => $faker->name, 'email' => 'graphql@test.com', 'password' => $password, ]); for ($i = 0; $i < 10; ++$i) { AppUser::create([ 'name' => $faker->name, 'email' => $faker->email, 'password' => $password, ]); } }
}
// database/seeds/ArticlesTableSeeder.php
use AppArticle;
use IlluminateDatabaseSeeder;
class ArticlesTableSeeder extends Seeder
{ /** * Run the database seeds. */ public function run() { AppArticle::truncate(); AppArticle::unguard(); $faker = FakerFactory::create(); AppUser::all()->each(function ($user) use ($faker) { foreach (range(1, 5) as $i) { AppArticle::create([ 'user_id' => $user->id, 'title' => $faker->sentence, 'content' => $faker->paragraphs(3, true), ]); } }); }
}
// database/seeds/DatabaseSeeder.php
use IlluminateDatabaseSeeder;
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 — это пакет, который я создал несколько лет назад. Недавно он получил удивительную поддержку со стороны растущего вокруг него сообщества. Он позволяет разработчикам быстро настроить сервер GraphQL, используя Laravel с небольшим количеством кода. При этом пакет достаточно гибкий и настраивается для использования в практически любом проекте.

Установим пакет в наш проект:

composer require nuwave/lighthouse:"3.1.*"

Опубликуем файл конфигурации Lighthouse:

php artisan vendor:publish --provider="NuwaveLighthouseLighthouseServiceProvider" --tag=config

Примечание. Вы также можете опубликовать дефолтную схему Lighthouse, просто удалив параметр --tag=config. Но в этой статье мы будем создавать схему с нуля.

Если вы посмотрите в файл config/lighthouse.php, то увидите настройку, используемую для регистрации нашего файла схемы в Lighthouse:

'schema' => [ 'register' => base_path('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, который имеет отношение к eloquent модели AppUser. Мы определили поля id, name и email, которые могут быть запрошены в модели User. Это также означает, что к полям password, created_at, и updated_at нельзя обращаться из API.

Далее у нас есть тип Query, являющийся точкой входа в наш API и использующийся для запроса данных. Наше первое поле — users, возвращающее массив типов объектов User. Директива @all сообщает Lighthouse, что нужно выполнить запрос Eloquent, используя модель User и получить все результаты. Тоже самое, что и:

$users = AppUser::all();

Примечание: Lighthouse знает, что модель нужно искать в пространстве имен AppUser по опции namespaces в файле конфигурации.

Второе поле в типе запроса — это вызов user, который принимает id в качестве аргумента и возвращает один тип объекта User. Мы также добавили две директивы, которые помогут Lighthouse автоматически построить запрос и вернуть один результат. Директива @eq сообщает Lighthouse, что нужно добавить where по id, а директива @find указывает на возврат одного результата. Если написать этот запрос, используя построитель запросов Laravel, то он будет выглядеть так:

$user = AppUser::where('id', $args['id'])->first();

Работаем с API GraphQL

Теперь, когда у нас есть представление о том, как Lighthouse использует нашу схему для создания запросов, давайте запустим наш сервер и запросим данные. Начнем с запуска нашего сервера:

php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

Чтобы запросить конечную точку GraphQL, вы можете запустить команду cURL в терминале или стандартном клиенте, например в Postman. Однако, для того, чтобы получить все преимущества GraphQL (такие как автозаполнение, выделение ошибок, документация и т.д.), мы будем использовать GraphQL Playground (скачать).

При запуске Playground, нажмите на вкладку «URL Endpoint» и введите http://localhost:8000/graphql, чтобы указать GraphQL Playground наш сервер. В левой части редактора мы можем запрашивать наши данные. Давайте начнем с того, что запросим всех пользователей, которых мы добавили в базу данных:

{ users { id email name }
}

Когда вы нажмете кнопку play в середине IDE (или Ctrl+Enter), то увидите ответ JSON от сервера с правой стороны, который будет выглядеть примерно так:

{ "data": { "users": [ { "id": "1", "email": "graphql@test.com", "name": "Carolyn Powlowski" }, { "id": "2", "email": "kheaney@yahoo.com", "name": "Elouise Raynor" }, { "id": "3", "email": "orlando.sipes@yahoo.com", "name": "Mrs. Dejah Wiza" }, ... ] }
}

Примечание. Поскольку мы использовали Faker для заполнения базы данных, то данные в полях email и name будут отличаться.

Давайте попробуем запросить одного пользователя:

{ user(id: 1) { email name }
}

И получим следующий ответ:

{ "data": { "user": { "email": "graphql@test.com", "name": "Carolyn Powlowski" } }
}

С запроса таких данных приятно начинать, но маловероятно, что вы окажетесь в проекте, где нужно будет запрашивать все данные разом. Поэтому добавим пагинацию. Если посмотреть на широкий спектр встроенных директив Lighthouse, то можно увидеть директиву @paginate. Обновим объект запроса нашей схемы следующим образом:

type Query { user(id: ID! @eq): User @find users: [User!]! @paginate
}

Если перезагрузить GraphQL Playground через Ctrl/Cmd+R и попробовать выполнить запрос пользователей еще раз, то вы получите ошибку «Cannot query field «id» on type «UserPaginator»». Что же случилось? Lighthouse манипулирует нашей схемой, чтобы мы могли получить постраничный вывод результатов, и делает это, изменяя тип возвращаемого поля users.

Давайте подробнее рассмотрим нашу схему на вкладке Docs в 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": "graphql@test.com" }, { "id": "2", "name": "Elouise Raynor", "email": "kheaney@yahoo.com" }, ] } }
}

Отношения

Как правило, при разработке приложений большая часть данных связана. В нашем случае User может написать много Articles, поэтому давайте добавим это отношение к типу User и определим тип Article:

type User { id: ID! name: String! email: String! articles: [Article!]! @hasMany
}
type Article { id: ID! title: String! content: String!
}

Здесь мы используем директиву @hasMany, которая сообщает Lighthouse, что модель User имеет отношение IlluminateDatabaseEloquentRelationsHasMany с моделью Article.

Давайте запросим свежеопределенные отношения:

{ 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." } ] } }
}

Теперь добавим отношение author к нашему типу объекта Article используя директиву @belongsTo и обновим Query:

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
}

Как вы видите, мы добавили опциональный аргумент relation в директиву @belongsTo. Он говорит Lighthouse использовать отношение user модели 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": "graphql@test.com" } }, { "id": "2", "title": "Voluptatem sed labore ea voluptas.", "author": { "name": "Carolyn Powlowski", "email": "graphql@test.com" } } ] } }
}

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, которая будет принимать заданные нами аргументы и создавать новую модель. Выполнение той же логики в контроллере будет выглядеть следующим образом:

namespace AppHttpControllers;
use IlluminateHttpRequest;
class UserController extends Controller
{ /** * Create a new user. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */ public function store(Request $request) { $data = $this->validate($request, [ 'email' => ['email', 'unique:users'], 'password' => ['min:6'] ]); $user = AppUser::create($data); return response()->json(['user' => $user]); }
}

Поле мутации createUser настроили, давайте запустим его в GraphQL Playground:

mutation { createUser( name:"John Doe" email:"john.doe@example.com" password: "secret" ) { id name email }
}

Мы должны получить следующий ответ:

{ "data": { "createUser": { "id": "12", "name": "John Doe", "email": "john.doe@example.com" } }
}

GraphQL Аутентификация и Авторизация

Поскольку нам нужно добавить user_id к моделям Article, сейчас самое время перейти к аутентификации и авторизации в GraphQL/Lighthouse.

Чтобы аутентифицировать пользователя, нам нужно предоставить ему api_token. Давайте создадим для этого мутацию и добавим директиву @field, указывающую Lighthouse на отдельный resolver, который будет обрабатывать эту логику. И настроим его как обычный контроллер в Laravel, используя аргумент resolver.

Директивой @field, определенной ниже, мы сообщаем Lighthouse, что когда запускается мутация login, то использовать метод createToken в AppGraphQLMutationsAuthMutator:

type Mutation { # ... login( email: String! password: String! ): String @field(resolver: "AuthMutator@resolve")
}

Примечание. Вам не нужно здесь использовать пространство имен. Для мутаций оно уже определено как App\GraphQL\Mutations в конфигурационном файле lighthouse.php. Однако вы можете использовать полное пространство имен, если хотите.

Давайте используем генератор Lighthouse для создания нового класса преобразователя (mutator):

php artisan lighthouse:mutation AuthMutator

Обновим нашу функцию resolver следующим образом:

namespace AppGraphQLMutations;
use IlluminateSupportArr;
use IlluminateSupportStr;
use IlluminateSupportFacadesAuth;
use GraphQLTypeDefinitionResolveInfo;
use NuwaveLighthouseSupportContractsGraphQLContext;
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 NuwaveLighthouseSupportContractsGraphQLContext $context Arbitrary data that is shared between all fields of a single query. * @param GraphQLTypeDefinitionResolveInfo $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; }
}

Теперь, когда resolver настроен, давайте его проверим и попробуем получить токен API, используя следующую мутацию в GraphQL Playground:

mutation { login(email:"graphql@test.com", password:"secret")
}

Мы должны получить токен:

{ "data": { "login": "VJCz1DCpmdvB9WatqvWbXBP2RN8geZQlrQatUnWIBJCdbAyTl3UsdOuio3VE" }
}

Примечание. Обязательно скопируйте присланный токен для того, чтобы его можно было использовать позже.

Для того, чтобы убедиться, что наша логика работает, давайте добавим поле запроса, которое вернет аутентифицированного пользователя. Поле под названием me и с директивой @auth. Установим аргумент guard равным api, поскольку именно так мы будем аутентифицировать пользователя.

Выполним запрос. В GraphQL Playground вы можете устанавливать заголовки ваших запросов, дважды кликнув на вкладку «Http Headers» внизу. Мы добавили заголовки с JSON объектом, поэтому для добавления bearer токена в каждый запрос надо добавить следующее:

{ "Authorization": "Bearer VJCz1DCpmdvB9WatqvWbXBP2RN8geZQlrQatUnWIBJCdbAyTl3UsdOuio3VE"
}

Примечание. Замените bearer токен на тот, который вы получили при логине.

Давайте выполним запрос me:

{ me { email articles { id title } }
}

В результате мы должны получить следующее:

{ "data": { "me": { "email": "graphql@test.com", "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 на наш resolver, а также включим директиву @middleware для гарантии, что пользователь залогинен.

type Mutation { # ... createArticle(title: String!, content: String!): Article @field(resolver: "ArticleMutator@create") @middleware(checks: ["auth:api"])
}

Во-первых, давайте сгенерируем класс преобразователя:

php artisan lighthouse:mutation ArticleMutator

И добавим в него следующую логику:

namespace AppGraphQLMutations;
use NuwaveLighthouseSupportContractsGraphQLContext;
class ArticleMutator
{ /** * Return a value for the field. * * @param null $rootValue * @param mixed[] $args * @param NuwaveLighthouseSupportContractsGraphQLContext $context * @return mixed */ public function create($rootValue, array $args, GraphQLContext $context) { $article = new AppArticle($args); $context->user()->articles()->save($article); return $article; }
}

Примечание. Мы переименовали дефолтную функцию resolve в create. Но не нужно создавать новый класс для каждого resolver, вы просто можете группировать логику.

Запустим нашу новую мутацию и проверим результат. Не забудьте сохранить заголовок Authorization из нашего предыдущего запроса на вкладке «HTTP Headers»:

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": "graphql@test.com" } } }
}

Завершение

Напомню, мы использовали Lighthouse для создания сервера GraphQL для нашего проекта на Laravel. Использовали встроенные директивы, создавали запросы и мутации, а также обрабатывали авторизацию и аутентификацию.

Lighthouse позволяет  делать гораздо больше (например, создавать собственные директивы), но в этой статье мы остановились на основах и смогли установить и запустить сервер GraphQL с небольшой шаблонной структурой.

В следующий раз, когда вам нужно настроить API для мобильного или одностраничного приложения, обязательно рассмотрите GraphQL как способ запроса ваших данных!

Автор: Christopher Moore
Перевод: Demiurge Ash

Источник: laravel.demiart.ru