Делаем GraphQL API на PHP и MySQL

в 7:17, , рубрики: graphql, mysql, php

image

В последнее время я все чаще и чаще слышу про 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» будем хранить список пользователей:

image

А в таблице «friendships» связи типа «многие-ко-многим», которые будут обозначать дружбу между пользователями:

image

Дамп базы данных, как и весь остальной код, можно взять из репозитория данной статьи на 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, поэтому сначала создадим этот тип.

Примечание

Также схема может содержать необязательный корневой тип данных Mutation, который предоставляет 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).

Примечание

Также тип данных может содержать свойства «interfaces», «isTypeOf» и «resolveField», но в рамках данной статьи мы их рассматривать не будем.

Поля типа данных также должны иметь обязательные свойства «name» и «type». Если свойство «name» не задано, то в качестве имени используется ключ поля (в данном случае «hello»). Также в нашем примере у поля «hello» заданы необязательные свойства «description» — описание и «resolve» — функция возвращающая результат. В этом случае функция «resolve» просто возвращает строку "Привет, GraphQL!", но в более сложной ситуации она может получать какую-либо информацию по API или из БД и обрабатывать ее.

Примечание

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

Таким образом, мы создали корневой тип данных «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 будет выглядеть так:

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») и выполним запрос:

image

Вывод пользователей из БД

Теперь усложним задачу. Выведем список пользователей из базы данных 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 необходимыми типами:

App/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, то я не буду объяснять как реализованы методы в данном классе, а просто приведу его код:

App/DB.php

<?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 будет таким:

App/Type/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»):

App/Type/QueryType.php

<?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 и проверить его работу таким запросом:

image

Или таким:

image

Или любым другим.

Заключение

На этом все. Читайте документацию. Задавайте вопросы в комментариях. Критикуйте.

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

Также рекомендую почитать исходный код с комментариями на Github.

Автор: Григорий Коваленко

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js