В последнее время я все чаще и чаще слышу про GraphQL. И в интернете уже можно найти немало статей о том как сделать свой GraphQL сервер. Но почти во всех этих статьях в качестве бэкенда используется Node.js.
Я ничего не имею против Node.js и сам с удовольствием использую его, но все-таки большую часть проектов я делаю на PHP. К тому же
В данной статье я хочу рассказать о том, как сделать свой GraphQL сервер на PHP с помощью библиотеки graphql-php и как с его помощью реализовать простое API для получения данных из MySQL.
Я решил отказаться от использования какого-либо конкретного PHP фреймворка в данной статье, но после ее прочтения вам не составит особого труда применить эти знания в своем приложении. Тем более для некоторых фреймворков уже есть свои библиотеки основанные на graphql-php, которые облегчат вашу работу.
Подготовка
В данной статье я не буду делать фронтенд, поэтому для удобного тестирования запросов к GraphQL серверу рекомендую установить GraphiQL-расширение для браузера.
Для Chrome это могут быть:
Также понадобится создать таблицы в БД и заполнить их тестовым набором данных.
В таблице «users» будем хранить список пользователей:
А в таблице «friendships» связи типа «многие-ко-многим», которые будут обозначать дружбу между пользователями:
Дамп базы данных, как и весь остальной код, можно взять из репозитория данной статьи на Github.
Hello, GraphQL!
Для начала необходимо установить graphql-php в наш проект. Можно сделать это с помощью composer:
composer require webonyx/graphql-php
Теперь, по традиции напишем «Hello, World».
Для этого в корне создадим файл graphql.php, который будет служить конечной точкой (endpoint) нашего GraphQL сервера.
В нем подключим автозагрузчик composer:
require_once __DIR__ . '/vendor/autoload.php';
Подключим GraphQL:
use GraphQLGraphQL;
Чтобы заставить GraphQL выполнить запрос необходимо передать ему сам запрос и схему данных.
Для получения запроса напишем следующий код:
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
Чтобы создать схему сначала подключим GraphQLSchema:
use GraphQLSchema;
Конструктор схемы принимает массив, в котором должен быть указан корневой тип данных Query, который служит для чтения данных вашего API, поэтому сначала создадим этот тип.
В простейшем случае тип данных Query должен быть экземпляром класса ObjectType, а его поля должны быть простых типов (например int или string), поэтому подключим классы предоставляющие эти типы данных в GraphQL:
use GraphQLTypeDefinitionType;
use GraphQLTypeDefinitionObjectType;
И создадим тип данных Query:
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'hello' => [
'type' => Type::string(),
'description' => 'Возвращает приветствие',
'resolve' => function () {
return 'Привет, GraphQL!';
}
]
]
]);
Как можно заметить тип данных обязательно должен содержать имя (name) и массив полей (fields), а также можно указать необязательное описание (description).
Поля типа данных также должны иметь обязательные свойства «name» и «type». Если свойство «name» не задано, то в качестве имени используется ключ поля (в данном случае «hello»). Также в нашем примере у поля «hello» заданы необязательные свойства «description» — описание и «resolve» — функция возвращающая результат. В этом случае функция «resolve» просто возвращает строку "Привет, GraphQL!", но в более сложной ситуации она может получать какую-либо информацию по API или из БД и обрабатывать ее.
Таким образом, мы создали корневой тип данных «Query», который содержит всего одно поле «hello», возвращающее простую строку текста. Давайте добавим его в схему данных:
$schema = new Schema([
'query' => $queryType
]);
А затем выполним запрос GraphQL для получения результата:
$result = GraphQL::execute($schema, $query);
Остается только вывести результат в виде JSON и наше приложение готово:
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
Обернем код в блок try-catch, для обработки ошибок и в итоге код файла graphql.php будет выглядеть так:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use GraphQLGraphQL;
use GraphQLSchema;
use GraphQLTypeDefinitionType;
use GraphQLTypeDefinitionObjectType;
try {
// Получение запроса
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
// Содание типа данных "Запрос"
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'hello' => [
'type' => Type::string(),
'description' => 'Возвращает приветствие',
'resolve' => function () {
return 'Привет, GraphQL!';
}
]
]
]);
// Создание схемы
$schema = new Schema([
'query' => $queryType
]);
// Выполнение запроса
$result = GraphQL::execute($schema, $query);
} catch (Exception $e) {
$result = [
'error' => [
'message' => $e->getMessage()
]
];
}
// Вывод результата
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
Проверим работу GraphQL. Для этого запустим расширение для GraphiQL, установим endpoint (в моем случае это «localhost/graphql.php») и выполним запрос:
Вывод пользователей из БД
Теперь усложним задачу. Выведем список пользователей из базы данных MySQL.
Для этого нам понадобится создать еще один тип данных класса ObjectType. Чтобы не нагромождать код в graphql.php, вынесем все типы данных в отдельные файлы. А чтобы у нас была возможность использовать типы данных внутри самих себя, оформим их в виде классов. Например, чтобы в типе данных «user» можно было добавить поле «friends», которое будет являться массивом пользователей такого же типа «user».
Когда мы оформляем тип данных в виде класса, то не обязательно указывать у него свойство «name», потому что оно по умолчанию берется из названия класса (например у класса QueryType будет имя Query).
Теперь корневой тип данных Query, который был в graphql.php:
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'hello' => [
'type' => Type::string(),
'description' => 'Возвращает приветствие',
'resolve' => function () {
return 'Привет, GraphQL!';
}
]
]
]);
Будет находиться в отдельном файле QueryType.php и выглядеть так:
class QueryType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => [
'hello' => [
'type' => Type::string(),
'description' => 'Возвращает приветствие',
'resolve' => function () {
return 'Привет, GraphQL!';
}
]
]
];
parent::__construct($config);
}
}
А чтобы в дальнейшем избежать бесконечной рекурсии при определении типов, в свойстве «fields» лучше всегда указывать не массив полей, а анонимную функцию, возвращающую массив полей:
class QueryType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => function() {
return [
'hello' => [
'type' => Type::string(),
'description' => 'Возвращает приветствие',
'resolve' => function () {
return 'Привет, GraphQL!';
}
]
];
}
];
parent::__construct($config);
}
}
При разработке проекта может появиться очень много типов данных, поэтому для них лучше создать отдельный реестр, который будет служить фабрикой для всех типов данных, в том числе и базовых, используемых в проекте. Давайте создадим папку App, а в ней файл, Types.php, который как раз и будет тем самым реестром для всех типов данных проекта.
Также в папке App создадим подпапку Type, в которой будем хранить все наши типы данных и перенесем в нее QueryType.php.
Теперь добавим пространство имен и заполним реестр Types.php необходимыми типами:
<?php
namespace App;
use AppTypeQueryType;
use GraphQLTypeDefinitionType;
class Types
{
private static $query;
public static function query()
{
return self::$query ?: (self::$query = new QueryType());
}
public static function string()
{
return Type::string();
}
}
Пока в нашем реестре будет всего 2 типа данных: 1 простой (string) и 1 составной (query).
Теперь во всех остальных файлах вместо:
use GraphQLTypeDefinitionType;
Подключим наш реестр типов:
use AppTypes;
И заменим все ранее указанные типы, на типы из реестра.
В QueryType.php вместо:
Type::string()
Будет:
Types::string()
А схема в graphql.php теперь будет выглядеть так:
$schema = new Schema([
'query' => Types::query()
]);
Чтобы получить пользователей из базы данных, необходимо обеспечить интерфейс доступа к ней. Получать данные из базы можно любым способом. В каждом фреймворке для этого есть свои инструменты. Для данной статьи я написал простейший интерфейс который может подключаться к MySQL базе данных и выполнять в ней запросы. Так как это не относится к GraphQL, то я не буду объяснять как реализованы методы в данном классе, а просто приведу его код:
<?php
namespace App;
class DB
{
private static $pdo;
public static function init($config)
{
self::$pdo = new PDO("mysql:host={$config['host']};dbname={$config['database']}", $config['username'], $config['password']);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
}
public static function selectOne($query)
{
$records = self::select($query);
return array_shift($records);
}
public static function select($query)
{
$statement = self::$pdo->query($query);
return $statement->fetchAll();
}
public static function affectingStatement($query)
{
$statement = self::$pdo->query($query);
return $statement->rowCount();
}
}
В файле graphql.php добавим код для инициализации подключения к БД:
// Настройки подключения к БД
$config = [
'host' => 'localhost',
'database' => 'gql',
'username' => 'root',
'password' => 'root'
];
// Инициализация соединения с БД
DB::init($config);
Теперь в папке Type создадим тип данных User, который будет отображать данные о пользователе. Код файла UserType.php будет таким:
<?php
namespace AppType;
use AppDB;
use AppTypes;
use GraphQLTypeDefinitionObjectType;
class UserType extends ObjectType
{
public function __construct()
{
$config = [
'description' => 'Пользователь',
'fields' => function() {
return [
'id' => [
'type' => Types::string(),
'description' => 'Идентификатор пользователя'
],
'name' => [
'type' => Types::string(),
'description' => 'Имя пользователя'
],
'email' => [
'type' => Types::string(),
'description' => 'E-mail пользователя'
],
'friends' => [
'type' => Types::listOf(Types::user()),
'description' => 'Друзья пользователя',
'resolve' => function ($root) {
return DB::select("SELECT u.* from friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}");
}
],
'countFriends' => [
'type' => Types::int(),
'description' => 'Количество друзей пользователя',
'resolve' => function ($root) {
return DB::affectingStatement("SELECT u.* from friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}");
}
]
];
}
];
parent::__construct($config);
}
}
Значение полей можно понять из их свойства «description». Свойства «id», «name», «email» и «countFriends» имеют простые типы, а свойство «friends» является списком друзей – таких же пользователей, поэтому имеет тип:
Types::listOf(Types::user())
Необходимо также добавить в наш реестр пару базовых типов, которые мы раньше не использовали:
public static function int()
{
return Type::int();
}
public static function listOf($type)
{
return Type::listOf($type);
}
И только, что созданный нами тип User:
private static $user;
public static function user()
{
return self::$user ?: (self::$user = new UserType());
}
Возвращаемые значения (resolve) для свойств «friends» и «countFriends» берутся из базы данных. Анонимная функция в «resolve» первым аргументом получает значение текущего поля ($root), из которого можно узнать id пользователя для вставки его в запрос списка друзей.
В завершении изменим код QueryType.php так, чтобы в API были поля для получения информации о конкретном пользователе по его идентификатору (поле «user»), а также для получения списка всех пользователей (поле «allUsers»):
<?php
namespace AppType;
use AppDB;
use AppTypes;
use GraphQLTypeDefinitionObjectType;
class QueryType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => function() {
return [
'user' => [
'type' => Types::user(),
'description' => 'Возвращает пользователя по id',
'args' => [
'id' => Types::int()
],
'resolve' => function ($root, $args) {
return DB::selectOne("SELECT * from users WHERE id = {$args['id']}");
}
],
'allUsers' => [
'type' => Types::listOf(Types::user()),
'description' => 'Список пользователей',
'resolve' => function () {
return DB::select('SELECT * from users');
}
]
];
}
];
parent::__construct($config);
}
}
Тут чтобы узнать идентификатор пользователя, данные которого необходимо получить, у поля «user» мы используем свойство «args», в котором содержится массив аргументов. Массив «args» передается в анонимную функцию «resolve» вторым аргументом, используя который мы узнаем id целевого пользователя.
'args' => [
'id' => Types::int()
]
Вместо:
'args' => [
'id' => [
'type' => Types::int()
]
]
Теперь можно запустить сервер GraphQL и проверить его работу таким запросом:
Или таким:
Или любым другим.
Заключение
На этом все. Читайте документацию. Задавайте вопросы в комментариях. Критикуйте.
Если данная статья будет интересной не только мне, то я напишу еще несколько статей про GraphQL на PHP, в которых постараюсь рассмотреть вопросы которые обошел стороной.
Также рекомендую почитать исходный код с комментариями на Github.
Автор: Григорий Коваленко