Часть 1. Сервер
Часть 2. Клиент
Часть 3. Мутации
Часть 4. Валидация. Выводы
Валидация и UnionType
Одной из интересных задач с которой пришлось столкнуться была серверная валидация при изменении данных. Как быть, если возникли ошибки при изменении объекта? В статьях можно найти много решений этой проблемы, но мы решили использовать композитный тип Union. Простыми словами, Union — это когда результат запроса может быть не одного лишь типа, а различных, в зависимости от результата выполнения resolve().
Приступим
Для примера добавим в наш объект User (см. Часть 1. Сервер) поле email, и сделаем так чтобы объект невозможно было сохранить с некорректным адресом.
(Напоминаю, что готовый проект к статье можно загрузить с github)
Шаг 1. Добавим поле в БД
Вернемся к серверу из части 1, и добавим нашему юзеру поле email. Для добавления поля в базу данных создадим миграцию:
$> yii migrate/create add_email_to_user
Откроем ее и изменим метод safeUp():
public function safeUp()
{
$this->addColumn('user', 'email', $this->string());
}
Сохраним и запустим
$> yii migrate
Шаг 2. Добавим правило в объект User
Единственно правильный способ реализации валидации в Yii это переопределением метода Model::rules(). Данный механизм предоставляет широчайшие возможности имплементации сколь угодно кастомизированных валидаций, и ко всем возможностям прилагается подробнейшая документация. Обходить ее стоит лишь в самых редких случаях, в 99% все возможно делать инструментами фреймворка.
/models/User.php:
...
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'email'], 'required'],
[['id', 'status'], 'integer'],
[['createDate', 'modityDate', 'lastVisitDate'], 'safe'],
[['firstname', 'lastname', 'email'], 'string', 'max' => 45],
['email', 'email'],
];
}
...
Таким образом, мы добавили 3 правила в метод rules():
- поле не может быть пустым;
- поле должно быть строкой;
- поле должно содержать валидный email.
Шаг 3. Обновим GraphQL-тип
В schema/UserType.php изменения минимальны:
...
'email' => [
'type' => Type::string(),
],
...
А вот в мутации начинается самое интересное.
Шаг 4. Добавление ValidationErrorType
Если вы не знакомы с фреймворком Yii, то уточню, что если объект не был сохранен по причине того, что не прошла валидация полей, то мы сможем вытащить все ошибки с помощью метода $object->getErrors(), который возвращает ассоциативный массив в формате:
[
'названиеПоля' => [
'Текст первой ошибки',
'Текст второй ошибки',
...
],
...
]
Формат очень удобный, но не для GraphQL. Дело в том, что выплюнуть непосредственно в JSON как есть мы это не можем по той причине, что ассоциативный массив преобразуется в объект, аттрибуты которого будут полями нашего объекта, а у каждого объекта они свои. А GraphQL, как известно, работает только с предсказуемым результатом. Как вариант, конечно, мы можем создвать отдельный тип для каждого объекта, что-то вроде UserValidationError, у которого все поля будут совпадать с полями самого объекта и содержать Type::listOf(Type::string()). Но ручное создание такого количества типов мне показалось чересчур неоптимальным, и я пошел другим путем.
Добавим единый класс с универсальной структурой schema/ValidationErrorType:
<?php
namespace appschema;
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
class ValidationErrorType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => function() {
return [
'field' => Type::string(),
'messages' => Type::listOf(Type::string()),
];
},
];
parent::__construct($config);
}
}
Данный тип содержит информацию об ошибке валидации для одного поля. Поля cамого типа ValidationErrorType, как по мне, очевидны и содержат название поля, в котором произошла ошибка, и список сообщений с содержанием ошибки. Все предельно просто.
Так как возвращать нам необходимо результат валидации всех полей, а не одного, создадим еще один тип: schema/ValidationErrorsListType:
<?php
namespace appschema;
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
class ValidationErrorsListType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => function() {
return [
'errors' => Type::listOf(Types::validationError()),
];
},
];
parent::__construct($config);
}
}
Шаг 5. Генерирование UnionType
В GraphQL существует тип Union, это когда результат resolve() поля может возвращать не один лишь тип, а один из нескольких (кажется я это уже писал, но здесь стоило об этом еще раз упомянуть). Таким образом наша цель сейчас состоит в том, чтобы результат мутации изменения/создания объекта, не пройдя или же наоборот, пройдя успешно валидацию возвращал либо сам измененный объект, либо объект типа ValidationErrorsListType.
Что означает "генерирование"? Дело в том, что мы не будем создавать для каждой мутации свой возвращаемый UnionType, а будем его генерировать на ходу, основываясь на базовом типе. Как именно, сейчас покажу.
Изменим наш schema/Types.php:
...
use GraphQLTypeDefinitionUnionType;
use yiibaseModel;
...
private static $validationError;
private static $validationErrorsList;
// здесь будут наши нагенеренные валидирующе типы
private static $valitationTypes;
...
// c этими двумя всё ясно
public static function validationError()
{
return self::$validationError ?: (self::$validationError = new ValidationErrorType());
}
public static function validationErrorsList()
{
return self::$validationErrorsList ?: (self::$validationErrorsList = new ValidationErrorsListType());
}
// метод возвращает новый сгенерированный тип, на основе
// типа, который пришел в аргументе
public static function validationErrorsUnionType(ObjectType $type)
{
// перво-наперво мы должны убедиться в том, что генерируем
// этот тип первый раз, иначе словим ошибку
// (я уже упоминал ранее о том, что одноименных/одинаковых
// типов в схеме GraphQL быть не может)
if (!isset(self::$valitationTypes[$type->name . 'ValidationErrorsType'])) {
// self::$valitationTypes будет хранить наши типы, чтобы не повторяться
self::$valitationTypes[$type->name . 'ValidationErrorsType'] = new UnionType([
// генерируем имя типа
'name' => $type->name . 'ValidationErrorsType',
// перечисляем какие типы мы объединяем
// (фактически мы их не объединяем, а говорим один из каких
// существующих типом вы будем возвращать)
'types' => [
$type,
Types::validationErrorsList(),
],
// в аргументе в resolveType
// в случае успеха нам придет наш
// сохраненный/измененный объект,
// в случае ошибок валидации
// придет ассоциативный массив из $model->getError()
// о котором я также упоминал
'resolveType' => function ($value) use ($type) {
if ($value instanceof Model) {
// пришел объект
return $type;
} else {
// пришел массив (ну или вообще неизвестно что,
// это нас уже мало волнует,
// хотя должен массив)
return Types::validationErrorsList();
}
}
]);
}
return self::$valitationTypes[$type->name . 'ValidationErrorsType'];
}
...
Шаг 6. Мутация
Внесем изменения в UserMutationType:
...
use appschemaTypes;
...
// изменим возвращаемый тип поля update
'type' => Types::validationErrorsUnionType(Types::user()),
...
// добавим еще один аргумент
'email' => Type::string(),
...
'resolve' => function(User $user, $args) {
// ну а здесь всё проще простого,
// т.к. библиотека уже все проверила за нас:
// есть ли у нас юзер, правильные ли у нас
// аргументы и всё ли пришло, что необходимо
$user->setAttributes($args);
if ($user->save()) {
return $user;
} else {
// на практике, этот весь код что ниже -
// переиспользуемый, и должен быть вынесен
// в отдельную библиотеку
foreach ($user->getErrors() as $field => $messages) {
// поля из ValidationErrorType
$errors[] = [
'field' => $field,
'messages' => $messages,
];
}
// возвращаемый формат ассоциативного
// массива должен соответствовать
// полям типа (в нашем случае ValidationErrorsListType)
return ['errors' => $errors];
}
}
...
Пришла пора увидеть, как же это работает.
Тестируем
Открываем снова наш GraphiQL и попробуем сделать что-нибудь самое простое.
Жмем в поле ввода Ctrl+Space, и смотрим, что нам может вообще возвращать метод update:
Awesome.
Ну и пробуем задать, что нам нужно, и смотрим, что получится.
Как видим, сработало первое правило, которое говорит о том, что поле — обязательное.
Правило 2 — поле email должно быть таковым в действительности. Валидация сработала.
Ну и посмотрим наконец на успешный результат:
В целом, я уверен, что описанный подход к валидации далеко не единственный оптимальный и несовершенный, его можно дорабатывать и улучшать под конкретные нужды, но в целом мне он показался весьма универсальным, и надеюсь кому-то он поможет на практике и натолкнет на решение возникшей проблемы.
Выводы
Чтобы понять, из чего именно мы делаем выводы, вам желательно было прочитать все 4 части.
Когда нужно и нужно ли вообще вам использовать GraphQL?
Ну во-первых, нужно понять, стоит ли игра свеч. Если у вас стоит задача написать пару-тройку несложных методов, конечно же, вам GraphQL не нужен. Это будет что-то вроде забивания гвоздей микроскопом. ИМХО, одно из главных преимуществ GraphQL это удобство масштабирования. А возможность масштабирования необходимо закладывать почти в любом проекте (а тем более в том проекте, в котором оно на первый взгляд вовсе не требуется).
Если ваше API "среднестатистическое", то тут, в принципе, разницы между выбранным протоколом вы не почувствуете. Мутации в GraphQL заменяете на экшны для RESTa (час-два вечером после работы под пиво), и оп — у вас уже RESTful API сервер.
Итак...
Очень низкий порог входа. Понять суть языка запросов GraphQL, найти необходимые библиотеки для backend и frontend под любой язык, разобраться как они работают — всё это займет у вас не больше дня (а то и нескольких часов).
Что-то новое. Новый опыт всегда полезен (даже негативный). Как известно, в области веб-технологий, обходя стороной новое, мы, как известно, деградируем.
Использование для внешнего (external) API. Если ваш конечный продукт это и есть API, которым пользуется большое количество клиентов (о которых вы ничего не знаете) GraphQL предоставляет огромную гибкость, так как у этих клиентов могут быть абсолютно разнообразные потребности. Но тут палка о двух концах. Технически, это преимущество, но, к сожалению, клиента может оттолкнуть то, что ему придется изучать что-то новое, и придется искать разработчиков с опытом работы с GraphQL API, ведь не все хотят нанимать разработчиков с обещанием выучить новую технологию в короткие сроки (несмотря на то, но в случае с GraphQL, эти сроки и в самом деле могут быть очень короткие).
Также GraphQL вам поможет в случае удаленной работы, и как следствие, отсутствии тесной коммуникации между backend и frontend разработчиками (ежедневные скайп-созвоны далеко не всегда гарантия "тесных" коммуникаций).
Из недостатков хотелось бы отметить мало примеров в сети по GraphQL + PHP, так как истинные смузихлёбы используют либо Node.js либо Go (что и подвигло меня написать эту серию статей). Та же ситуация с библиотекой Apollo, вся официальная документация по которой написана под React, и мне, как разработчику backend, необходимо было потратить время, чтобы понять, как это всё работает с Polymer, хотя не назвал бы это большой проблемой при переходе. Кстати советую почитать очень содержательный блог Apollo на Medium. Там действительно много интересных и практических статей по GraphQL.
Также одним из недостатков является отсутствие удобного генератора документации, подобного Swagger или ApiDoc.js. Один генератор мне таки найти удалось, но он, к сожалению, весьма убог. Если у вас есть опыт более продвинутого документирования чем описание в pdf, прошу поделиться в комментариях.
Кто уже использует GraphQL из известных компаний?
GitHub. Вцелом ничего удивительного, так как целевая аудитория сайта — разработчики, и никаких переживаний по поводу того, смогут ли их API использовать возникнуть не могло. Стоит отметить что документация выполнена очень красиво и продуманно, и что примечательно, содержит немного информации об основах GraphQL, как его дебажить и гайды по миграции с REST.
Facebook. Являются разработчиком концепции самого GraphQL, и активно его продвигают. В сетях есть много докладов на тему того, как Facebook использует GraphQL.
P.S.: Друзья! Никогда не забывайте имплементировать обработку запросов с методом OPTIONS в своем API, чтобы на все такие запросы сервер всегда возвращал http-код 200, и пустое тело. Это сохранит вам нервные клетки.
И вообще про хедеры для CORS не забывайте при разработке любого API:
<IfModule mod_headers.c>
Header add Access-Control-Allow-Origin "*"
Header add Access-Control-Allow-Headers "Content-Type, Authorization"
Header add Access-Control-Allow-Methods "GET, POST, OPTIONS"
</IfModule>
Автор: timur560