
В последнее время я все чаще и чаще слышу про GraphQL. И в интернете уже можно найти немало статей о том как сделать свой GraphQL сервер. Но почти во всех этих статьях в качестве бэкенда используется Node.js.
Я ничего не имею против Node.js и сам с удовольствием использую его, но все-таки большую часть проектов я делаю на PHP. К тому же хостинг с PHP и MySQL гораздо дешевле и доступнее чем хостинг с Node.js. Поэтому мне кажется не справедливым тот факт, что об использовании GraphQL на 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 GraphQL\GraphQL;
Чтобы заставить GraphQL выполнить запрос необходимо передать ему сам запрос и схему данных.
Для получения запроса напишем следующий код:
$rawInput = file_get_contents('php://input'); $input = json_decode($rawInput, true); $query = $input['query'];
Чтобы создать схему сначала подключим GraphQL\Schema:
use GraphQL\Schema;
Конструктор схемы принимает массив, в котором должен быть указан корневой тип данных Query, который служит для чтения данных вашего API, поэтому сначала создадим этот тип.
В простейшем случае тип данных Query должен быть экземпляром класса ObjectType, а его поля должны быть простых типов (например int или string), поэтому подключим классы предоставляющие эти типы данных в GraphQL:
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ObjectType;
И создадим тип данных 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 будет выглядеть так:
require_once __DIR__ . '/vendor/autoload.php'; use GraphQL\GraphQL; use GraphQL\Schema; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\ObjectType; 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 необходимыми типами:
namespace App; use App\Type\QueryType; use GraphQL\Type\Definition\Type; 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 GraphQL\Type\Definition\Type;
Подключим наш реестр типов:
use App\Types;
И заменим все ранее указанные типы, на типы из реестра.
В QueryType.php вместо:
Type::string()
Будет:
Types::string()
А схема в graphql.php теперь будет выглядеть так:
$schema = new Schema([ 'query' => Types::query() ]);
Чтобы получить пользователей из базы данных, необходимо обеспечить интерфейс доступа к ней. Получать данные из базы можно любым способом. В каждом фреймворке для этого есть свои инструменты. Для данной статьи я написал простейший интерфейс который может подключаться к MySQL базе данных и выполнять в ней запросы. Так как это не относится к GraphQL, то я не буду объяснять как реализованы методы в данном классе, а просто приведу его код:
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 будет таким:
namespace App\Type; use App\DB; use App\Types; use GraphQL\Type\Definition\ObjectType; 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»):
namespace App\Type; use App\DB; use App\Types; use GraphQL\Type\Definition\ObjectType; 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.
ссылка на оригинал статьи https://habrahabr.ru/post/328122/
Добавить комментарий