Пишем GraphQL API сервер на Yii2 с клиентом на Polymer + Apollo. Часть 1. Сервер

Часть 1. Сервер
Часть 2. Клиент
Часть 3. Мутации
Часть 4. Валидация. Выводы

Статья рассчитана на широкий круг читателей и требует лишь базовых знаний PHP и Javascript. Если вы занимались программированием и вам знакома аббревиатура API, то вы по адресу.

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

И сразу же хочу добавить, что не считаю GraphQL панацеей от всех бед и киллером RESTful API.

Кто мы?


Мы — компания, которая разрабатывает мобильные приложения, причем, как правило, у этих приложений существует клиент на iOS (ну понятное дело), Android и Web. Лично я в этой компании занимаюсь написанием серверной части на PHP.

Предыстория


Все началось с того что мы закончили разработку одного приложения (ну как закончили, закончить разработку приложения невозможно, просто его финансирование было безвременно приостановлено), и нам тут же зашел новый проект. Благо, компания не ограничивает разработчиков в выборе технологий (конечно же в разумных пределах), и было принято решение внести определенные изменения, дабы попытаться избежать проблем, возникавших в прошлом. Ведь, как известно, если продолжать делать то, что делаешь, то и продолжаешь получать то, что получаешь.

Проблемы RESTful API


Не то чтобы у REST’а были большие проблемы, но с одной из них я сталкивался весьма регулярно. Дело в том, что разработчики на наших проектах весьма высоко квалифицированы, и это, в свою очередь, причина того, что каждый из них считает себя на определенном уровне экспертом в своей области. API это та тонкая ниточка среди технологий которая связывает backend и frontend специалистов и является причиной многочисленных споров о том, как именно его нужно разрабатывать.

Структура проекта


Для примера рассмотрим типичную структуру данных:

image

Тут нужно понимать, что в реальной жизни такие таблицы могут иметь по 20+ полей, что делает использование GraphQL еще более привлекательным и оправданным. Почему именно, я попытаюсь объяснить в статье.

Какие API методы хочет создавать backend разработчик?


Разработчик серверной части конечно же хочет писать API методы таким образом, чтобы они максимально соответствовали целостным исчерпывающим объектам. Например:

GET /api/user/:id

{
	id
	email
	firstName
	lastName
	createDate
	modifyDate
	lastVisitDate
	status
}

GET /api/address/:id

{
	id
	userId
	street
	zip
	cityId
	createDate
	modifyDate
	status
}

GET /api/city/:id

{
	id
	name
}

… и т.д. Написание подобной архитектуры не только проще и быстрее (если не говорить о том, что это все делает за нас какой-нибудь скаффолдинг), но и архитектурно и эстетически красивее и правильнее (по крайней мере так считает сам разработчик). В лучшем случае backend соглашается на вложенные объекты, чтобы вместо addressId в респонсе возвращался вложенный объект адреса или (в случае связки “один ко многим”) массив адресов.

Какие API методы хочет вызывать разработчик UI?


Разработчик клиента немногим более приближен к живым людям (пользователям приложения) и их потребностям, в результате чего в приложении существует несколько (нужно читать много) мест, где необходимы разные наборы одних и тех же данных. Таким образом он хочет для каждого функционального элемента иметь по методу:

GET /api/listOfUsersForMainScreen

[
	{
		firstName
		lastName
		dateCreated
		city
		street
	}
	...
]

… и так далее в том же духе. Конечно же, такое желание вполне оправдано не только желанием сократить себе работу, но и улучшить производительность приложения. Во-первых UI делает один вызов вместо трёх (сначала user, затем address, а потом и city). Во вторых, такой метод избавит от получения множества (зачастую немалого) избыточных данных. При этом очень желательно чтобы dateCreated возвращалась в человеческом формате, а не в первозданном, взятом из поля в БД (а то ведь еще придется и unix time конвертировать).

На этой почве и рождаются конфликты, коих я был свидетелем, а иногда и участником. Хорошие разработчики ищут и, конечно же, находят компромиссы, чтобы частично удовлетворить потребности друг друга. Но если у вас, к примеру, один frontend и два backend, то тут одиночке придется включить свою харизму на максимум и задействовать все дипломатические навыки, дабы продавить написание своих бессмысленных методов, которые будут вызываться один раз за весь жизненный цикл приложения.

Что такое GraphQL и почему он должен решать мои проблемы?


Для тех, кто не знаком с GraphQL, советую потратить не более, чем 5-10 минут и посетить эту страницу, чтобы понять с чем его едят.

Почему он решает вышеописанную проблему? Потому что при использовании GraphQL, серверный разработчик описывает атомарные сущности и связи так, как ему это нравится, а UI строит кастомные запросы в зависимости от потребностей конкретного элемента. И вроде бы и овцы сыты и волки целы, но, к счастью, мы не живем в идеальном мире и подстраиваться все равно приходится. Но обо всем по-порядку.

Покажите мне код


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

Готовый демо проект серверной части можно посмотреть тут.

Собственно, приступим к написанию сервера. Чтобы пропустить настройку фреймворка и окружения, которая не содержит никакой информации о самом GraphQL, можете сразу переходить к созданию структуры (шаг 2).

Шаг 1. Установка и настройка Yii2


Данный шаг никак не связан с GraphQL, но он необходим для наших дальнейших действий.

Установка и настройка Yii2
Я просто оставлю это здесь (обязательно к выполнению).

После всего проделанного поднимем базочку. Команда

$> yii migrate

… создаст в БД таблицу для истории миграций, а…

$> yii migrate/create init

… создаст новый файл для миграции в директории migrations (init — название миграции, может быть любым). В созданный файл положим следующий исходный код:

<?php

use yii\db\Migration;

class m170828_175739_init extends Migration
{
    public function safeUp()
    {
        $this->execute("CREATE TABLE IF NOT EXISTS `user` (
          `id` INT NOT NULL,
          `firstname` VARCHAR(45) NULL,
          `lastname` VARCHAR(45) NULL,
          `createDate` DATETIME NULL,
          `modityDate` DATETIME NULL,
          `lastVisitDate` DATETIME NULL,
          `status` INT NULL,
          PRIMARY KEY (`id`))
        ENGINE = InnoDB;");

        $this->execute("CREATE TABLE IF NOT EXISTS `city` (
          `id` INT NOT NULL,
          `name` VARCHAR(45) NULL,
          PRIMARY KEY (`id`))
        ENGINE = InnoDB;");

        $this->execute("CREATE TABLE IF NOT EXISTS `address` (
          `id` INT NOT NULL,
          `street` VARCHAR(45) NULL,
          `zip` VARCHAR(45) NULL,
          `createDate` DATETIME NULL,
          `modifyDate` DATETIME NULL,
          `status` INT NULL,
          `userId` INT NOT NULL,
          `cityId` INT NOT NULL,
          PRIMARY KEY (`id`),
          INDEX `fk_address_user_idx` (`userId` ASC),
          INDEX `fk_address_city1_idx` (`cityId` ASC),
          CONSTRAINT `fk_address_user`
            FOREIGN KEY (`userId`)
            REFERENCES `user` (`id`)
            ON DELETE NO ACTION
            ON UPDATE NO ACTION,
          CONSTRAINT `fk_address_city1`
            FOREIGN KEY (`cityId`)
            REFERENCES `city` (`id`)
            ON DELETE NO ACTION
            ON UPDATE NO ACTION)
        ENGINE = InnoDB;");
    }

    public function safeDown()
    {
        echo "m170828_175739_init cannot be reverted.\n";

        return false;
    }
}

… и запустим миграцию:

$> yii migrate


Примечание. Сам считаю дурным тоном создавать структуру базы данных в обход средств, предоставляемых фреймворком и на практике никогда так не делаю. В данном примере это лишь сэкономило мне время. Это же касается пренебрежительно оставленной мной пустой функции для ролл-бэка safeDown().

Примечание. Для теста нам понадобится немного тестовых данных в нашей базе. Вы можете сами ее наполнить или взять миграцию с тестового репозитория.

На основе созданной структуры данных сгенерируем модели (ActiveRecord) с помощью встроенного во фреймворк генератора Gii:

$> yii gii/model --tableName=user --modelClass=User
$> yii gii/model --tableName=city --modelClass=City
$> yii gii/model --tableName=address --modelClass=Address

Наконец-то вся скучная работа позади, и теперь пора заняться тем, ради чего мы это всё затеяли.

Шаг 2. Установка расширения для GraphQL


Для нашего проекта будем использовать базовое расширение webonyx/graphql-php (https://github.com/webonyx/graphql-php).

$> composer require webonyx/graphql-php

Также на github можно найти уже заточенное расширение под Yii2, но на первый взгляд оно меня не вдохновило. Если же вы с ним знакомы, поделитесь своим опытом в комментариях.

Шаг 3. Создаем структуру проекта.


Основные элементы структуры, задействованные в реализации GraphQL сервера:

schema — директория в корне фреймворка, которая будет хранить сущности для GraphQL сервера: типы и мутации. Название директории и расположение не принципиально, можете назвать как угодно и расположить в другом неймспейсе (например api/ или components/).

schema/QueryType.php, schema/MutationType.php — “корневые” типы.

schema/Types.php — некий агрегатор для инициализации наших кастомных типов.

schema/mutations — мутации предпочтительно хранить в отдельной директории для удобства.

Ну и собственно controllers/api/GraphqlController.php — точка входа. Все запросы к GraphQL серверу идут через одну точку входа — /api/graphql. Таким образом ничего не мешает вам параллельно содержать RESTful API (если уж на то пошло, то, грубо говоря, GraphQL сервер это один API-метод, принимающий на вход параметрами GraphQL-запросы).

Шаг 3.1. Создадим типы для наших моделей.


Создадим новую директорию schema и в ней классы для наших моделей.

/schema/CityType.php:

<?php

namespace app\schema;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class CityType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => function() {
                return [
                	'name' => [
                		'type' => Type::string(),
                	],
                ];
            }
        ];

        parent::__construct($config);
    }

}

В описании поля участвуют следующие параметры:

type — GraphQL-тип поля (Type::string(), Type::int(), т.д).

description — описание (просто текст; будет использоваться в схеме, придает удобство при отладке запросов).

args — принимаемые аргументы (ассоциативные массив, где ключ — имя аргумента, значение — GraphQL-тип).

resolve($root, $args) — функция, которая возвращает значение поля. Аргументы: $root — объект соответствующего ActiveRecord (в данном случае в него будет приходить объект models\City); $args — ассоциативный массив аргументов (описанных в $args).

Все поля кроме type — опциональны.

/schema/UserType.php:

<?php

namespace app\schema;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use app\models\User;

class UserType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => function() {
                return [
                    'firstname' => [
                        'type' => Type::string(),
                    ],
                    'lastname' => [
                        'type' => Type::string(),
                    ],
                    'createDate' => [
                        'type' => Type::string(),
                        
                        // текстовое описание, поясняющее
                        // что именно хранит поле
                        // немного позже вы увидите в чем его удобство
                        // (оно еще больше сократит ваше общение с юайщиком)
                        'description' => 'Date when user was created',
                        
                        // чтобы можно было форматировать дату, добавим
                        // дополнительный аргумент format
                        'args' => [
                            'format' => Type::string(),
                        ],

                        // и собственно опишем что с этим аргументом
                        // делать
                        'resolve' => function(User $user, $args) {
                            if (isset($args['format'])) {
                                return date($args['format'], strtotime($user->createDate));
                            }

                            // коли ничего в format не пришло, 
                            // оставляем как есть
                            return $user->createDate;
                        },
                    ],

                    // при необходимости с остальными датами можно
                    // произвести те же действия, но мы
                    // сейчас этого делать, конечно же, не будем
                    'modityDate' => [
                        'type' => Type::string(),
                    ],
                    'lastVisitDate' => [
                        'type' => Type::string(),
                    ],
                    'status' => [
                        'type' => Type::int(),
                    ],

                    // теперь самая интересная часть схемы - 
                    // связи
                    'addresses' => [
                        // так как адресов у нас много,
                        // то нам необходимо применить
                        // модификатор Type::listOf, который
                        // указывает на то, что поле должно вернуть
                        // массив объектов типа, указанного
                        // в скобках
                        'type' => Type::listOf(Types::address()),
                        'resolve' => function(User $user) {
                            // примечательно то, что мы можем сразу же
                            // обращаться к переменной $user без дополнительных проверок
                            // вроде, не пустой ли он, и т.п.
                            // так как если бы он был пустой, до текущего
                            // уровня вложенности мы бы просто не дошли
                            return $user->addresses;
                        },
                    ],
                ];
            }
        ];

        parent::__construct($config);
    }

}

/schema/AddressType.php:

<?php

namespace app\schema;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class AddressType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => function() {
                return [
                    'user' => [
                        'type' => Types::user(),
                    ],
                    'city' => [
                        'type' => Types::city(),
                    ],

                    // остальные поля не столь интересны
                    // посему оставляю их вам на 
                    // личное растерзание
                ];
            }
        ];

        parent::__construct($config);
    }

}

Для AddressType.php нам необходим вспомогательный класс Types.php, который описан ниже.

Шаг 3.2. schema/Types.php


Дело в том, что GraphQL схема не может иметь несколько одинаковых (одноименных) типов. Именно за этим призван следить агрегатор Types.php. Название нарочно было выбрано именно Types, чтобы было похоже, и, в тоже время, отличалось от стандартного класса библиотеки GraphQL — Type. Таким образом обратиться к стандартному типу можно через Type::int(), Type::string(), а к кастомному — Types::query(), Types::user(), и т.д.

schema/Types.php:

<?php 

namespace app\schema;

use GraphQL\Type\Definition\ObjectType;

class Types
{
    private static $query;
    private static $mutation;

    private static $user;
    private static $address;
    private static $city;


    public static function query()
    {
        return self::$query ?: (self::$query = new QueryType());
    }

    public static function user()
    {
        return self::$user ?: (self::$user = new UserType());
    }

    public static function address()
    {
        return self::$address ?: (self::$address = new AddressType());
    }

    public static function city()
    {
        return self::$city ?: (self::$city = new CityType());
    }

}

Шаг 3.3. schema/QueryType.php


<?php

namespace app\schema;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use app\models\User;
use app\models\Address;

class QueryType extends ObjectType
{
    public function __construct()
    {
        $config = [
            'fields' => function() {
                return [
                    'user' => [
                        'type' => Types::user(),

                        // добавим сюда аргументов, дабы
                        // выбрать необходимого нам юзера
                        'args' => [
                            // чтобы агрумент сделать обязательным
                            // применим модификатор Type::nonNull()
                            'id' => Type::nonNull(Type::int()),
                        ],
                        'resolve' => function($root, $args) {
                            // таким образом тут мы уверены в том
                            // что в $args обязательно присутствет элемент с индексом
                            // `id`, и он обязательно целочисленный, иначе мы бы сюда не попали

                            // так же мы не боимся, что юзера с этим `id`
                            // в базе у нас не существует
                            // библиотека корректно это обработает
                            return User::find()->where(['id' => $args['id']])->one();
                        }
                    ],

                    // в принципе на поле user можно остановиться, в случае
                    // если нам нужно обращаться к данным лиш конкретного пользователя
                    // но если нам нужны данные с другими привязками добавим
                    // для примера еще полей

                    'addresses' => [
                        // без дополтинельных параметров
                        // просто вернет нам списох всех
                        // адресов
                        'type' => Type::listOf(Types::address()), 

                        // добавим фильтров для интереса
                        'args' => [
                            'zip' => Type::string(),
                            'street' => Type::string(),
                        ],
                        'resolve' => function($root, $args) {
                            $query = Address::find();

                            if (!empty($args)) {
                                $query->where($args);
                            }

                            return $query->all();
                        }
                    ],
                ];
            }
        ];

        parent::__construct($config);
    }
}

C MutationType.php разберемся немного позже.

Шаг 3.4. Создаем controllers/api/GraphqlController.php


Теперь доделаем последнюю часть, чтобы к нашим новосозданным типам можно было достучаться.

GraphqlController.php:

<?php

namespace app\controllers\api;

use app\schema\Types;
use GraphQL\GraphQL;
use GraphQL\Schema;
use yii\base\InvalidParamException;
use yii\helpers\Json;

class GraphqlController extends \yii\rest\ActiveController
{
    public $modelClass = '';

    /**
     * @inheritdoc
     */
    protected function verbs()
    {
        return [
            'index' => ['POST'],
        ];
    }

    public function actions()
    {
        return [];
    }

    public function actionIndex()
    {
        // сразу заложим возможность принимать параметры
        // как через MULTIPART, так и через POST/GET

        $query = \Yii::$app->request->get('query', \Yii::$app->request->post('query'));
        $variables = \Yii::$app->request->get('variables', \Yii::$app->request->post('variables'));
        $operation = \Yii::$app->request->get('operation', \Yii::$app->request->post('operation', null));

        if (empty($query)) {
            $rawInput = file_get_contents('php://input');
            $input = json_decode($rawInput, true);
            $query = $input['query'];
            $variables = isset($input['variables']) ? $input['variables'] : [];
            $operation = isset($input['operation']) ? $input['operation'] : null;
        }

        // библиотека принимает в variables либо null, либо ассоциативный массив
        // на строку будет ругаться

        if (!empty($variables) && !is_array($variables)) {
            try {
                $variables = Json::decode($variables);
            } catch (InvalidParamException $e) {
                $variables = null;
            }
        }

        // создаем схему и подключаем к ней наши корневые типы

        $schema = new Schema([
            'query' => Types::query(),
        ]);

        // огонь!

        $result = GraphQL::execute(
            $schema,
            $query,
            null,
            null,
            empty($variables) ? null : $variables,
            empty($operation) ? null : $operation
        );

        return $result;
    }
}

Также стоит отметить, что обертывание в try-catch GraphQL::execute() для форматирования вывода ошибок ничего не даст, т.к. он уже внутри перехватывает всё возможное, а что делать с ошибками, я опишу немного позже.

Шаг 4. Тестируем.


Собственно, настало время осознать, увидеть и потрогать то, что у нас получилось.

Проверим наш запрос в расширении для Chome — GraphiQL. Лично я больше предпочитаю «GraphiQL Feen», которое имеет более расширенный функционал (сохраненные запросы, кастомные хедеры). Правда последнее иногда имеет проблемы с выводом ошибок, а точнее просто ничего не выводит в случае ошибки на сервере.

Вводим в поля необходимые данные и радуемся результату:

image

Таким образом, после всего имеем:

  • автокомплит по полям и аргументам (с подчеркиванием неверных параметров, конечно же)
  • вывод дескрипшна подсвеченного поля
  • автоматическая справка с серфингом в правой части
  • моментальное получение результата

Примечание. Если у вас не заработал красивый URL, это значит, что вы недоконфигурили UrlManager и .htaccess, т.к. в первозданном Yii это не работает. Как это сделать гляньте в репозитории к статье.

Автокомплит аргументов:

image

Полностью кастомный запрос с полностью кастомным результатом — мечта frontend разработчика:

image

Могли ли вы об этом всем мечтать разрабатывая свой RESTful, при том что вы для этого ничего и не делали? Конечно же нет.

Также важно обратить внимание на то, что для того, чтобы вытащить адреса и пользователя, нам не нужно делать два отдельных запроса, а можно (и нужно) всё сделать сразу:

image

Возможность дебага запросов лично я считаю одним из весомых преимуществ GraphQL. Дело в том, что он автоматически генерирует схему, которую втягивает расширение, и у вас включается валидация и автокомплит. Таким образом имея лишь адрес точки входа в GraphQL сервер, вы можете полностью изучить его возможности. Несекьюрно? Как по мне, секьюрность реализуется немного на другом уровне. Имея доступ к документации любого API, мы точно так же имеем полную его схему. Некоторые RESTful API имеют подобные JSON или XML схемы, к сожалению не многие, а для GraphQL это стандарт, к тому они же активно используются клиентами.

To be continued…


В следующей(их) части(ях) статьи я опишу как это всё по-красивому использовать в UI, и конечно же мы затронем еще одну тему без которой API не API — это мутации.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 39
  • +1
    У вас в проекте пользователь может загрузить картинки? Если да, то вы делаете это через GraphQL?
    • 0
      Интересный вопрос. Да, такая задача в перспективе есть, но еще до нее не добрались. Да и на крайний случай, никто не запретит нам это реализовать мимо GraphQL, хотя возможно это не очень красиво.
      • +1
        Можно попробовать картинку грузить в виде base64 строки
        • –1
          В js можно получить доступ к загружаемому файлу? Даже если и да, то кажется способ не очень хороший. Всё же грузить файлы нужно мультипартом.

          Возможно вы путаете с загрузкой с сервера, а тут говорется о загрузке на сервер.
          • +2
            • 0
              Ну да, но не уверен, что там не будет ограничений в формате или размере запроса. На практике я такое не использовал, и думаю что нет сложности отправить файл стандартным способом. Думаю что доступ к загружаемому файлу из js больше необходим для какого-нибудь препроцессинга и валидации.
              • +1
                Ограничения могут быть только со стороны бекенда, но это же ваш бекенд)
                • 0
                  Возможно вы правы, но нужно пробовать. Да и основной concern не в ограничениях, а в корректности такого подхода.
                  • 0

                    Нормальный подход. В Stay.com так API работал для картинок.

                    • 0
                      А как решали то что в base64 размер больше?
                      • 0

                        Игнорировали.

                        • 0
                          Эм, ну у вас же должны были быть каккие-то лимиты?
                          • +1
                            Гигабайты дешевые
                            • 0

                              50 мегабайт на фото, если верно помню. Хватало на тот момент.

            • 0
              А какие проблемы и преимущества есть при использовании base64?
              • +1
                Проблемы… Ну наверно саппорт старых браузеров и необходимость писать больше js кода.
                Преимущества:
                1. Простота АПИ
                2. Возможность грузить файлы не только с диска, но и с памяти (например пользователь нарисовал что-то на холсте и тут же зааплоадил в галерею)
                3. Возможность грузить файлы в ПОСТ запросах с application/json и т.п
                • 0
                  Как проблему я вижу только то что base64 на 30% больше размер.
                • 0
                  Мое субъективное мнение заключается в том, что для каждой функции есть свой механизм, своя технология. Upload файлов нативно реализован через multipart, так как именно он для этого предназначен. Я думаю были причины не совать данные непосредственно в тело поста. Обход нативных механизмов всегда плох, и должен применяться только если действительно нет возможности воспользоваться стандартным способом.
                  • +2
                    Мультипарт — это просто исторический факт. Если бы он был идеален, то не было бы различных флеш-загрузчиков и позже разнообразных аякс-аплоадеров (которые на самом деле просто сабмитят форму в невидимый фрейм) и прочих костылей.
            • 0
              Хорошая статья, для тех кто не знал с какой стороны зайти, спасибо. Мутации я так понимаю будут реализованы аналогично запросу, с сохранением данных в resolve?
              • 0
                Спасибо. Да, так и есть, в целом реализация мутаций аналогична, за исключением некоторых моментов.
                • 0
                  Да, читал о них, и их преимуществах, но, к сожалению, пока не дошли руки попробовать.
                • 0
                  Здравствуйте. А подскажите как вы обрабатываете исключения и выводите исключения? С глобальными в принципе понятно — есть определенный метод через который проходят все исключения — их там можно обработать, добавить всякие плюшки вроде requestId, timestamp… А как быть с мутациями где мы проверяем и сохраняем поля в базу данных или third-party сервис — обрабатывать в каждой мутации все исключения которые могут в ней возникнуть?
                  • 0
                    Да! Именно об этом я уже пишу в следующей части статьи. Тоже столкнулся с разными решениями этой проблемы, и то, которое было выбрано, считаю очень лаконичным и оптимальным. Если котортко то для этого я использовал тип Union, когда результат может быть разных типов, один из которых это кастомный ValidationErrorsType, который хранит массив текстов ошибок.
                    • 0
                      а когда примерно ждать статью по мутациям?
                      • 0
                        Надеюсь на выходных выделить время, и тогда максимум к понедельнику закончу. Но возмжоно и раньше. Как пойдет.
                        • 0
                          habrahabr.ru/post/337236 — как и обещал, правда с задержкой.
                    • 0
                      Есть 2 вопроса:
                      1. Возможно ли реализовать вывод деревянной структуры? к примеру список подразделений (одно приложение может входить в другое) не в виде плоского списка с идентификатором родителя, а в виде дерева. Если возможно (могу сделать рекурсивный запрос) но как будет выглядеть запрос с неизвестным уровнем вложенности?

                      {
                        "data": {
                          "offices": [
                            "office": {
                              "naz" : "Офис 1",
                              "offices" : [
                                     "office": {
                                            "naz" : "Офис 1.1",
                                            "offices" : [
                                                  ...
                                            ]
                                     },
                                     "office": {
                                            "naz" : "Офис 1.2"
                                     }
                               ] 
                               },
                              "office": {
                              "naz" : "Офис 1"
                               },
                              ...
                            ]
                          }
                        }
                      }


                      2. Возможно ли вывести нумерованный массив? listOf позволяет выводить массивы, но индексы игнорируются. К сожалению изменить структуру и вынести индекс в атрибут не возможно.

                      {
                        "data": {
                          "operative_control": {
                            "que": [
                              "1" : {
                                "R": "4",
                                 ...
                              },
                              "26" : {
                                "R": "6",
                                 ...
                              }
                              ...
                            ]
                          }
                        }
                      }
                      • 0
                        Присоединяюсь к вопросу. Я вот не смог сообразить как это сделать с MySql, чтобы не было кучи лишних запросов. С mongodb это прокатывает, но с реляционной базой данной придется попотеть.
                        • 0
                          Насчет кучи лишних запросов это отдельная задача. Тут нужна тонкая настройка под фреймворк. Yii2 очень хорошо справляется с оптимизацией запросов, если правильно его использовать.
                        • 0
                          1. Неизвестный уровень вложенности, насколько я знаю, невозможен. В этом и суть GraphQL, чтобы получать предсказуемый респонс. Но я не очень понимаю смысла возвращать неизвестный результат. Это проблема не GraphQL, а вашей архитектуры API, когда клиент заранее не знает, что получит. Возможно для каких то очень кастомных случаев. Но тогда REST вам в помощь.
                          Если мы говорим о рекурсии, то да, она возможна, и я с ней столкнулся на своем проекте, очень занимательно выходит с использованием GraphQL. А сам запрос выходит примерно таким:
                          query {
                          	user {
                          		firstName
                          		lastName
                          		address {
                          			zip
                          			street
                          		}
                          		relatedUsers {
                          			firstName
                          			lastName
                          			address {
                          				zip
                          				street
                          			}
                          		}
                          	}
                          }
                          


                          … где поле relatedUser имеет тоже тип User как и сам user.

                          2. У вас изначально некорректная структура JSON. Дело в том что существует список [] и объект {}. Объект это подобие ассоциативного массива, либо же объекта в PHP. Объект содержит атрибуты ключ-значение. Список не может иметь индексов или ключей. Список это список. К конкретному элементу вы обращаетесь лишь по его порядковому номеру в списке, начиная с 0. Т.е. структура, которую вы описываете:
                          ...
                          [
                          "23": { ... }
                          ]
                          ...
                          


                          просто некорректна синтаксически. А задача с тем что вы говорите, решается именно выносом индекса в атрибут.
                          • 0
                            Спасибо за ответ. По 2 вопросу я согласен с вами, но к сожалению именно такую структуру требуется создать.
                            По рекурсии я немного не пойму. Вы привели пример не рекурсии а двухуровнего объекта, где у пользователя могут быть подчиненные (один уровень). А рекурсия это бесконечная вложенность пока не дойдем до дна. Т.е. рекурсия это кладр: регион, район, город, населенный пункт, городской район. Притом на любом уровне может быть обрыв (например у региона обрывается на первом уровне, у города может на 2 может на 3 (если этот город в районе)).
                            P.S. Кладр я как пример привел, т.к. там по гуидам нужно подниматься в верх, пока не дойдем до региона
                            • 0
                              Ваш пример «регион, район, город, населенный пункт, городской район» это не рекурсия. И дна, о котором вы говорите, у рекурсии нет. Да, я описал лишь два уровня, но GraphQL не позволить вам сделать запрос с неизвестным результатом, или неизвестной вложенностью, о чем я и говорил. А рекурсия в моем примере как раз потому, что User содержит поле типа User, но уровень вложенности вы описываете в запросе сами, т.е.:
                              query {
                              	user {
                              		name
                              		relatedUser {
                              			name
                              			relatedUser {
                              				name
                              				relatedUser {
                              					name
                              					relatedUser {
                              
                              						...
                              
                              					}
                              				}
                              			}
                              		}
                              	}
                              }
                              
                        • 0
                          Конечно же, такое желание вполне оправдано не только желанием сократить себе работу, но и улучшить производительность приложения.
                          Не всегда хорошо грузить одним запросом не связанные между собой данные. Во-первых — размер. Придется пользователю подождать и смотреть на пустой экран, пока не загрузятся данные.
                          • 0
                            Тут говорилось в контексте того, что фронтенд-разработчик желает получать лишь необходимые данные, а не всё подряд.
                            • 0
                              Да но можно получить лишь часть данных и показать их пользователю, а остальные подруэить.
                              • 0
                                Можно и так. В этом и суть, что в случае с GraphQL решение стоит лишь за фронтенд разработчиком, — он может делить запросы на сколько угодно частей.
                          • 0
                            Можно и так. В этом и суть, что в случае с GraphQL решение стоит лишь за фронтенд разработчиком, — он может делить запросы на сколько угодно частей.

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