Строим взаимодействие систем на PHP с помощью GraphQL
Как построить систему для взаимодействия между разными PHP-модулями и приложениями при помощи инструмента GraphQL.
3К открытий3К показов
Человечество уже давно выработало для себя множество различных систем коммуникации: речь, символы, жесты, письменность и т. д., которые позволяют представителям разных сообществ находить общий язык. Информационные системы в компаниях тоже нуждаются в возможностях для взаимодействия: приеме и передаче информации понятным для всех элементов образом. Сегодня поговорим о том, как построить архитектуру таких взаимодействий, используя GraphQL.
Сергей Тарасов
К. т. н., руководитель команд разработки, Группа НЛМК
Владимир Шершнев
Старший разработчик, Группа НЛМК
Небольшой дисклеймер
Статья в основном рассчитана на архитекторов/интеграторов/аналитиков/разработчиков, которые не просто рассматривают информационную систему с точки зрения «кода», а учитывают полный жизненный цикл, включая поддержку, развитие, систему управления знаниями и т. д.
Материал носит информационный характер, основанный на опыте и реальных системах, которые успешно введены в промышленную эксплуатацию, и не призывает слепо использовать те или иные технологии и методы.
Что такое или кто такой ЕКП?
ЕКП — это пример информационной системы Группы НЛМК. Расшифровывается как Единый корпоративный портал, единое окно предоставления цифровых услуг всем сотрудникам Группы.
Немого информации о портале:
- >100 различных интерфейсов взаимодействия с другими системами;
- >25 серверов с различными ландшафтами и функциями;
- языки программирования: бэк — PHP, фронт — JS, HTML, CSS;
- три языка: французский, датский, русский;более 80 сервисов из различных функциональных направлений, которые сами по себе являются полноценными ИС со своей бизнес-логикой, механизмами интеграций и дорожными картами (roadmap):
- новости,
- сообщества,
- блоги,
- лендинги,
- адресная книга и структура организации,
- прием делегаций,
- заказ переводческих услуг,
- расчетный лист,
- карта выявления опасностей,
- поведенческие диалоги безопасности,
- корпоративный университет,
- сервис оповещений,
- система управления знаниями — HR-гид и т. д.;
- более 30 сайтов-визиток для различных ведомств и департаментов Группы;
- >46 000 уникальных пользователей портала;
- >4500 департаментов в организационной структуре.
Интеграционные взаимодействия
Изначально портал (~2017 год) был в основном потребителем информации, т. е. забирал данные из других систем и понемногу формировал свою базу знаний с помощью уникальных сервисов, которые были реализованы только на нем. Интеграционные взаимодействия, как правило, строились на базе REST-архитектуры с использованием curl или своих обработчиков на базе протокола https. Это был осознанный выбор, базировался он на следующих принципах.
- Относительно невысокий порог вхождения в архитектуру REST и написание «простых» http(s) запросов. На рынке уже есть множество различных решений:
1. стандартные библиотеки и функционал PHP (curl, http-запросы);
2. библиотеки с подробными мануалами по использованию (Guzzle, Slim и т. д.).
- Гибкость — под каждую интеграцию можно написать свою реализацию, например, обмен с помощью XML, json и других объектов.
- Необходимость научить новую систему сначала «играть по текущим правилам», прежде чем их менять или диктовать свои. Портал только создавался и выступал в качестве «потребителя», а ряд систем с мастер-данными существовали уже давно со своими устоявшимися процессами и правилами (например, кадровый источник информации на базе SAP HCM).
По мере обогащения информацией и формирования собственной уникальной базы знаний стали появляться «потребители» — другие ИС. Информация была им нужна для различных целей: некоторым для популяризации своего сервиса путем вывода новостей/блогов/сообществ о компании, некоторым для расширения собственной базы знаний о сотрудниках и т. д. Таким образом, ЕКП стал не только «потребителем», но и «поставщиком».
Далее в качестве примера рассмотрим интеграцию с мобильным приложением Группы на базе получения/публикации новостей и сопроводительной информации по ним (рубрики, комментарии, просмотры и т. д.).
«Потребитель» VS «поставщик» с точки зрения ИС
Если раньше портал понимал, откуда забирать информацию и в каком виде, то в роли «поставщика» появилась большая неизвестность, стало неясно:
- количество возможных «потребителей»;
- платформа и технологический стек «потребителя», т. е. непонятно, как с ним взаимодействовать;
- для чего и какая информация может потребоваться.
Исходя из вышеописанного, можно сделать вывод, что необходимо разработать собственные и единые «правила игры» для всех потенциальных «потребителей«.
Правила игры
Система-поставщик должна:
- Быть единообразной: работать одинаково для всех «потребителей» без исключения.
- Использовать один запрос на получение всей необходимой информации (one-step).
«Step-by-Step» VS «One-Step»
Объекты предоставляемой информации имеют связность с другими объектами. Например, у каждой новости есть рубрика, комментарии, просмотры и авторы (рис. 1).
Информацию можно получать двумя различными способами (рис. 2).
- Step-by-step — шаг за шагом, т. е. формируя несколько запросов, из которых складывается общая картина. Например, получить новость ->рубрики->просмотры->комментарии и т. д.
- One-step — за один шаг, т. е. формируя один запрос. Например, получить новость сразу со всеми связанными элементами.
Множество HTTP-запросов по каждому объекту (step-by-step) вызовет:
- снижение производительности — из-за высокой нагрузки на сеть растет время анализа запросов и формирования ответов, а вместе с этим снижается скорость работы для конечного пользователя и т. д.;
- сложность поддержки — из-за множества методов под каждый объект и их кросc-функционального взаимодействия.
Поэтому необходим такой язык взаимодействия, который бы позволял однозначно сформулировать на входе все свои требования, а на выходе получать результат (one-step).
GraphQL
В ходе поиска инструмента под вышеописанные правила даже были попытки разработать собственный, но в конечном итоге выбор пал на GraphQL.
GraphQL — это язык запросов с открытым исходным кодом.
Три основные характеристики языка:
- Позволяет клиенту точно указать, какие данные ему нужны.
- Облегчает агрегацию данных из нескольких источников.
- Использует систему типов для описания данных.
В качестве основы мы стали использовать библиотеку GraphQL, которая соответствует изначально заложенной спецификации, имеет неплохую документацию и постоянно развивается.
Процесс установки зависимостей достаточно хорошо описан в документации, поэтому перейдем сразу к правилам.
Конечная точка GraphQL
Для того чтобы мы могли принимать запросы, определим единую точку входу graphql: mysite.ru/graphql/. Для этого создаем путь /graphql/index.php. Данный файл является единой точкой входа, который подключает библиотеку GraphQL и может содержать в себе следующую логику.
- Создание схемы — используется класс GraphQLTypeSchema, в конструктор класса необходимо передать типы данных для запросов query и mutation.
- Обработка запроса — используется метод GraphQLGraphQL:executeQuery, в конструктор класса необходимо передать созданную схему и полученный запрос.
- Обработка ошибок — в случае ошибки выбрасывается исключение. Исключения являются экземплярами класса GrahpQlException. В случае отлова исключения в ответе возвращается статус ошибки, сообщение ошибки и место возникновения ошибки.
- Вывод ответа — ответ отдается в формате json, в случае успеха ключом json-массива будет data, если было выброшено исключение, ключом массива будет errors.
Ниже еще рассмотрим некоторые сценарии более детально.
Для передачи запросов в наших примерах будем использовать приложение «Postman». Для работы с ним существует много документации, интерфейс интуитивно понятен, потому эту тему затрагивать не будем.
Для обработки запросов в /graphql/index.php добавим следующий код:
Вначале мы подключаем автозагрузчик. Оборачиваем в try catch наш код для отлова ошибок и возвращаем результат в формате json.
Внутри try у нас добавлена инициализация схемы. Схема может иметь несколько типов запросов:
Query — запрос на получение;
Mutation — запрос на изменение.
Для начала реализуем запрос на получение данных.
Типы запросов GraphQL — Query
Для удобства создаем необходимую структуру файлов и директорий:
QueryType.php — список Query-запросов.
Создадим тестовую базу данных (БД) и наполним ее данными. Получилась следующая схема:
Добавим следующий код в файл AppTypeQueryType.php :
Конструктор класса «QueryType» будет содержать в себе все подтипы «query»-запросов, он должен быть наследован от «ObjectType», что позволяет нам создавать свои составные типы данных.
В данном примере у нас возвращается запрос «news», который имеет тип «Types::listOf(Types::news())» — т.е. список новостей. В качестве принимаемых параметров фильтра «args» у нас передается «id», тип «Types::int()».
Ключ «resolve» возвращает результат «News::get($args)».
Создадим класс для работы с сущностью «News» и опишем возвращаемые поля запроса NewsType.
Создадим файл AppTypeNewsType.php и добавим в него код:
Теперь нам необходимо реализовать метод «News::get($args)» для возврата результата.
Создадим файл AppEntityNews.php и добавим в него код:
Ключи возвращаемого массива должны быть в нижнем регистре, чтобы произошел корректный маппинг, так как в таблице мы добавили ключи в БД в верхнем регистре, а описали в нижнем.
В данном примере конвертация из верхнего регистра в нижний производится в методе $DB->query и детально не рассматривается.
Отправим тестовый запрос на получение новостей из приложения:
В результате запроса мы получили ответ в виде json-объекта. Мы запросили возврат полей id, name, text (эти поля мы описали в файле «AppTypeNewsType.php«).
Составной запрос (step-by-step & one-step)
Если мы посмотрим на рисунок 4, где изображена наша схема базы данных, то увидим, что, зная ID новости, мы можем получить рубрику.
Добавим еще один запрос на получение рубрик в файл AppTypeQueryType.php :
Затем регистрируем наш новый тип данных в файле AppTypeTypes.php, и он принимает уже следующий вид:
Так как при добавлении типа rubric мы вызвали конструктор «new RubricType()», необходимо описать поля этого типа данных.
В файле AppTypeRubricType.php опишем возвращаемые поля согласно таблице «rubrics»:
Добавляем, как уже делали ранее, класс для работы с сущностью Rubric и реализуем метод Rubric::get($args) в файле AppEntityRubric.php:
Для проверки отправим запрос на получение рубрик:
Нам вернулся корректный ответ, но сейчас, чтобы получить рубрику новости, необходимо послать два запроса: сначала получить поля новости, затем послать запрос на получение рубрики, зная ее ID (step-by-step).
Реализуем составной запрос, чтобы мы могли получить эти же данные при одном запросе (one-step).
Поменяем код в файле AppTypeNewsType.php там, где у нас были описаны возвращаемые поля типа «rubric»:
Отправляем запрос на получение новости и указываем поля рубрики, которые хотим вернуть:
В результате запроса мы получили объект новости, у которого был выполнен составной запрос на получение рубрики.
Теперь мы можем получать как отдельно рубрику, указав ее ID в параметрах запроса (рис. 6), так и составным запросом (рис. 7).
По аналогии мы можем реализовать получение автора новости, просмотры и комментарии.
Типы запросов GraphQL — Mutation
Запросы типа Mutation позволяют менять данные в нашей БД.
Добавим возможность обрабатывать запросы на изменение в нашем файле /graphql/index.php, изменив схему:
По аналогии c Query-типом зарегистрируем наш тип в файле AppTypeTypes.php:
После этого создаем файл AppTypeMutationType.php. В нем опишем тип запроса «add_comment» для добавления комментария к новости:
Зарегистрируем тип «comments» в файле AppTypeTypes.php:
Опишем возвращаемые поля для комментариев в файле AppTypeCommentType.php:
Добавляем класс для работы с сущностью «комментарий» AppEntityComments.php и реализуем в нем метод «Comments::set»:
Отправляем запрос на добавление комментария:
В результате запроса мы получили поля добавленного комментария. Таким образом мы реализовали запрос типа «Mutation».
Типы данных (Input & Output)
В GraphQL типы данных делятся на:
- Output type — типы для вывода данных (возвращаемых полей);
- Input type — типы для ввода данных (передаваемых аргументов).
Простые типы данных относятся к обоим видам одновременно (Scalar, Enum, List, NonNull).
Составной тип Object, который мы использовали в наших примерах, относится к Output. Но есть тип InputObject, который является типом для передачи аргументов. InputObject отличается от Object-типа тем, что его поля не могут иметь «args» и «resolver».
Создадим тип данных CommentInputType в файле AppTypeCommentInputType.php для передачи аргументов:
Зарегистрируем наш тип данных AppTypeTypes.php:
Поменяем код в нашем файле мутаций AppTypeMutationType.php:
Отправляем запрос:
Теперь в запросе мы можем передавать полноценный объект комментария, поля и значения которого будут отправлены в обработку.
Выводы и рекомендации
- Определите назначение системы в разрезе поставщик/потребитель.
- Не ищите «волшебную пилюлю», которая сможет решить все проблемы. Каждая технология по-своему хороша.
- Анализируйте рынок, возможно, вы найдете уже готовое решение или базовую основу для достижения какой-либо цели.
- Отдавайте собственную информацию. Собственная информация — данные, которые хранятся в самой системе, при этом неважно, как они в нее попали. Например, «поставщик» получает из кадровой системы информацию по пользователям с полями ФИО, должность, орг. структура и сохраняет их в системе, но «потребителю» нужен еще табельный номер, который есть у «кадров», но его нет у «поставщика». В этом случае, если нужна дополнительная информация из других ИС. Лучше интегрироваться напрямую с ними без «посредников», которые являются дополнительными точками отказа и возникновения проблем; используйте отдельные прокси-сервера/мосты/шины данных (брокеры сообщений) для построения взаимодействий.
- Старайтесь отдавать «сырую» информацию, т. е. забирать ее напрямую из хранилища без модификаций. Все модификации должен делать «потребитель» на своей стороне. Почему? Как было написано выше, мы не знаем, как и для чего «потребитель» может использовать нашу информацию.
3К открытий3К показов