3 сентября 2017

Drupal 8 и GraphQL. Часть 1

Алёна Парфирьева
Разработчик

Документации по модулю GraphQL для Drupal очень мало, а та, что есть не успевает за разработкой новых версий модуля. В этих двух статьях я поделюсь своим опытом и тем, что смогла найти, читая Stack overflow и issues на Github. Модуль развивается и, возможно, мои изыскания быстро устареют. Но, тем не менее, если хоть одному человеку я смогу упростить жизнь этими статьями, то я буду рада.

Ниже мы рассмотрим как связать GraphQL и Drupal, попробуем поработать со схемой и кастомными полями. 

Drupal 8 и GraphQL. Часть 1:

Drupal 8 и GraphQL. Часть 2

Первые шаги

Установим модуль GraphQL для Drupal с помощью Composer:

composer require drupal/graphql

Получаем вот такую картину:

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

На момент написания статьи это была незадокументированная фича, нашла я ее, изучая issues на Github. Для того, чтобы можно было получать нормально поля какого-либо Entity, мы должны создать View Mode для GraphQL.

Создадим View Mode для Content и для Taxonomy. Идем в /admin/structure/display-modes/view и создаем новый View mode. Название должно быть обязательно GraphQL, регистр не важен.

Таким образом мы можем регулировать какие поля попадают в схему запроса, а какие нет. Ничего не понятно? Давайте посмотрим практический пример.

Давайте создадим список работников компании. Я создаю таксономию — словарь Person с четырьми работниками.

Для каждого термина словаря у меня есть 3 дополнительных поля: фотография работника, его должность и поле для внутреннего использования, которое мы не хотим показывать в запросе от front-end — может ли работник есть варенье бесплатно. Так как Карлсон у нас летает на варенье, то для него эта опция будет отмечена.

Так как же спрятать поле в запросе? Заходим во вкладку Manage display и разворачиваем внизу CUSTOM DISPLAY SETTINGS

Отмечаем созданный ранее Display Mode — GraphQL. После того, как мы сохраним настройки, вверху появится еще один display. Перейдя на него, мы сможем управлять видимостью полей в запросе — просто перетащите ненужное поле в секцию Disabled.

Тут же мы можем изменить формат данных, то как они будут выводиться в запросе. Например, чтобы получить путь к изображению, меняем формат на URL to image:

Сохраняем настройки. Время сделать наш первый запрос GraphQL!

Первый запрос

Мы готовы написать наш первый запрос! Но перед этим мы должны включить в схему, созданную ранее таксономию. 

Настройки модуля мы можем найти тут: /admin/config/graphql. Заходим в Exposed content, ищем и отмечаем Taxonomy Term и Person. Чтобы можно было запрашивать кастомные поля, необходимо выбрать в колонке ATTACH FIELDS FROM VIEW MODE созданный ранее View Mode

Сохраняем настройки и наконец-то переходим к делу! Заходим в /graphql/explorer. В левой части мы будем писать запрос, справа получать результат. Также справа есть вкладка Docs. Раскрыв ее, мы получим доступ ко всем доступным нам запросам. На данный момент их не очень много, но с помощью этих запросов, вы можете получить любую ноду или любой термин. В принципе, для написания простейших запросов, этого уже достаточно. Но нам нужно нечто более сложное. Как писать запросы с параметрами или с постраничным выводом, я расскажу чуть позже. А пока давайте попробуем запросить данные по созданной таксономии.

В схеме есть такой запрос:

taxonomyTermQuery(
    offset: Int = 0
    limit: Int = 10
    filter: TaxonomyTermQueryFilterInput
): EntityQueryResult!

Filter — это параметры запроса, если мы перейдем к TaxonomyTermQueryFilterInput, то получим доступные нам параметры:

tid: Int
uuid: String
langcode: String
vid: String
name: String
description: String
weight: Int
parent: Int
changed: String
defaultLangcode: Boolean

Будем делать запрос по параметру vid, так мы знаем его значение (person). Чтобы понять, какие поля мы можем запросить, посмотрим EntityQueryResult:

count: Int
entities: [Entity]

И Entity:

entityId: String
entityLabel: String
entityLanguage: Language
entityOwner: Entity
entityPublished: Boolean
entityCreated: String
entityChanged: String
entityBundle: String
entityUrl: Url
entityType: String
entityUuid: String
entityTranslation(language: AvailableLanguages!): Entity

Давайте запросим общее количество работников, имя работника и его id. В левой части пишем запрос:

query Person {
  taxonomyTermQuery(filter: {vid: "person"}) {
    count
    entities {
      entityLabel
    }
  }
}

Получаем:

Обратите внимание, что я переопределила название запроса и поля entityLabel. В противном случае, мы бы получили такой ответ:

Отлично, мы выполнили первый запрос. Но, если мы посмотрим на доступные поля, которые можем получить, мы не увидим кастомных полей фото и должности. Для этого мы должны указать тип запрашиваемой Entity.

Если справа в Docs набрать в поиске Person, то увидим нужный нам тип:

И в нем созданные нами поля:

entityId: String
entityLabel: String
entityLanguage: Language
entityOwner: Entity
entityPublished: Boolean
entityCreated: String
entityChanged: String
entityBundle: String
entityUrl: Url
entityType: String
entityUuid: String
description: String
fieldPhoto: String
fieldPosition: String
name: String
entityTranslation(language: AvailableLanguages!): Entity

Обратите внимание, что поля допуска к варенью нет, чего мы и добивались!

Укажем этот тип в запросе и запросим нужные поля:

Давайте сразу оформим фрагмент для запроса.

Что такое фрагменты, читайте в документации GraphQL

Перепишем запрос таким образом:

query Person {
  staff: taxonomyTermQuery(filter: {vid: "person"}) {
    count
    persons: entities {
      ...person
    }
  }
}

fragment person on TaxonomyTermPerson {
  name: entityLabel
  photo: fieldPhoto
  position: fieldPosition
}

Первый же вопрос, который у меня возник — а как это все сортировать? Или, допустим, я хочу выбрать только курьеров? Что делать в этом случае? А тут нам придет на помощь функционал Views.

Расширяем схему с помощью Views

Давайте создадим новый View для таксономии Person:

И сразу же добавим новый Display для GraphQL:

Пока ничего не будем добавлять, запрос будет выбирать все термины, принадлежащие Person. Вы можете поменять название запроса в настройках View:

Назовем его personList. Кстати, в той версии модуля, что была у меня при разработке, этого пункта не было, а название можно было поменять только вот тут:

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

Важно! После каждого изменения и сохранения View обязательно чистите кеш Drupal, иначе ваши изменения не будут видны в GraphiQL.

Переходим обратно к эксплореру, ищем в поиске person:

Ура, наш запрос появился. Используя уже созданный фрагмент, можно переписать запрос:

query Person {
  personList {
    ...person
  }
}

Получаем:

Давайте отсортируем список по должности. Добавляем во View новый Sort criteria:

Не забываем чистить кеш. Переходим в explorer, перезапускаем запрос и вуаля!

Давайте теперь создадим запрос с параметром. Будем фильтровать по должности. В том же View создадим новый display типа GraphQL. Добавим Filter criteria по field_position (не забудьте указать, что создавать фильтр надо только для этого display — this graphql (override). При создании фильтра отметьте галку Expose this filter to visitors. Выбираем оператор Is equal to, чтобы фильтровать по значению должности. Запрос назовем personByPosition:

Если поискать в схеме, то мы видим, что к запросу добавился параметр filter:

Который состоит из field_position_value: String.

Перепишем запрос:

query Person {
  personList(filter: {field_position_value: "founder"}) {
    ...person
  }
}

Ну и напоследок, сделаем запрос для постраничного вывода. Создадим еще один Display GraphQL, назовем запрос personByPage. В группе ADVANCED зададим такие параметры:

Количество выводимых элементов указывает на значение по умолчанию. В самом запросе этот параметр можно поменять. Если мы посмотрим созданную схему, то увидим:

PersonGraphqlPersonByPageResult
arguments
page: Int = 0
pageSize: Int = 2

Сам запрос запишем так:

query Person($page: Int = 0, $pageSize: Int = 2) {
  personByPage(page: $page, pageSize: $pageSize) {
    count
    results {
      ...person
    }
  }
}

Сделаем запрос с параметром:

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

Кастомные поля в схеме

В процессе разработки мне часто приходилось сталкиваться с такими ситуациями, когда нужно было получить поля не предусмотренные схемой. Или написать целый запрос, который нельзя получить средствами View. Все, что я буду описывать далее, я делала по аналогии полей самого модуля GraphQL, не могу ручаться за правильность такого подхода, но других вариантов я не нашла. То, что написано в документации или в примерах в issue проекта на Github, не работает.

Итак, допустим нам необходимо получить weight термина. Этого мы не можем сделать в стандартной схеме. Для того, чтобы расширить схемы, напишем свой модуль graphql_custom_fields. Структура директорий:

В папке Fields у нас и будут все наши кастомные поля. Само поле описывается так:

namespace Drupal\graphql_custom_fields\Plugin\GraphQL\Fields;

use Drupal\taxonomy\Entity\Term;
use Drupal\graphql_core\GraphQL\FieldPluginBase;
use Youshido\GraphQL\Execution\ResolveInfo;

/**
 * GraphQL field resolving a Terms's weight.
 *
 * @GraphQLField(
 *   id = "taxonomy_term_weight",
 *   name = "weight",
 *   type = "Int",
 *   types = {"Entity"}
 * )
 */
class TaxonomyWeight extends FieldPluginBase {
  public function resolveValues($value, array $args, ResolveInfo $info) {
    if ($value instanceof Term) {
      yield intval($value->getWeight());
    } else {
      yield null;
    }
  }
}

Важные для нас поля:

  1. name — то, как поле будет называться в запросе
  2. type — тип поля
  3. types — то, в каких типах запроса может это поле появляться. Несколько типов можно указать через запятую, например: {"Entity", “MyAwesomeType”}

Для того, чтобы передать полю нужное значение, нам надо реализовать функцию resolveValues. Главными для нас тут являются переменные $value и $args.

$value — это сам термин или то, что получается в результате запроса. В нашем конкретном примере мы запрашиваем термин таксономии Person, вот он и будет в этой переменной.

$args — переменные запроса, о них поговорим позже. 

Так как Entity у нас может быть все, что угодно, например Node, то делаем проверку, является ли полученное значение термином таксономии. Если является, то возвращаем weight переданного термина.

Устанавливаем модуль, чистим кеш и не забываем выставить weight у терминов. Момент истины! Выполняем запрос, добавили в фрагмент новое поле:

query Person {
  personList {
    ...person
  }
}

Теперь нам доступен вес термина!