WEB змінюється дуже швидко. Ще декілька років тому головним mainstream & best practise для реалізації API вважався REST, то сьогодні він вже має багато недоліків і всі використовують GraphQL, як єдино правильний варіант для роботи через API.
Якщо серйозно, то в цих словах багато сарказму, кожен вирішує сам, що є кращим для нього. Свого часу з появою NoSQL баз даних (Mongo) весь Інтернет гудів, що от ми знайшли срібну кулю від усіх бід і тепер заживемо щасливо. Мало того, багато хто ще тоді похоронив SQL бази даних і очікував швидкої їх смерті. Пройшло порядку 10 років, MySQL, MSSQL, PostgreSQL та інші SQL бази даних не вмерли, вони випускають нові релізи і додають в них нові плюшки.
Єдине що дійсно, не потрібно відставати від тенденцій і при нагоді пробувати нові технології, хто зна можливо вони вам сподобаються.
Загальні відомості про GraphQL
GraphQL водночас є простою і складною. Простою вона може бути для клієнта, який отримує дані з сервера. Щодо грамотного налаштування серверної частини, то це може вимагати багато сил, і є доволі складною процедурою.
Одним з головних аргументів чому GraphQL is true при виборі платформи для реалізації API є те, що єдина точнка входу в ваше API залишається не змінною, ви просто нарощуєте функціонал і клієнт за потреби його використовує. В тому ж REST при реалізації нових можливостей потрібно було б змінити версію в URL, наприклад /api/v1/ -> /api/v2/.
Налаштування
Наявна структура з Doctrine моделей в короткі терміни дає можливість отримати базову версію API, потрібно просто передати моделі в модуль GraphQL Doctrine
і вони відображаться в Schema
.
Doctrine Модель
Проста, без зв’язків модель для отримання інформації по маркетплейсах.
namespace Stagem\Amazon\Model;
use Doctrine\ORM\Mapping as ORM;
use GraphQL\Doctrine\Annotation as API;
use Popov\ZfcCore\Model\DomainAwareTrait;
use Stagem\ZfcPool\Model\PoolInterface;
/**
* @ORM\Entity(repositoryClass="Stagem\Amazon\Model\Repository\MarketplaceRepository")
* @ORM\Table(name="amazon_marketplace")
*/
class Marketplace implements PoolInterface
{
const MNEMO = 'marketplace';
const TABLE = 'amazon_marketplace';
use DomainAwareTrait;
/**
* @var int
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer", options={"unsigned":true})
*/
private $id;
/**
* @var string
* @ORM\Column(type="string", length=14, unique=true, options={"unsigned":true})
*/
private $code;
/**
* @var string
* @ORM\Column(type="string", nullable=true, length=255)
*/
private $name;
/**
* @var string
* @ORM\Column(type="string", nullable=true, unique=true, length=255)
*/
private $domain;
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @param string $id
* @return Marketplace
*/
public function setId($id): Marketplace
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getCode(): string
{
return $this->code;
}
/**
* @param string $code
* @return Marketplace
*/
public function setCode(string $code): Marketplace
{
$this->code = $code;
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return Marketplace
*/
public function setName(string $name): Marketplace
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getDomain(): string
{
return $this->domain;
}
/**
* @param string $domain
* @return Marketplace
*/
public function setDomain(string $domain): Marketplace
{
$this->domain = $domain;
return $this;
}
/**
* @return string
*/
public function getCountryCode(): string
{
return trim(str_replace(['www', 'amazon'], '', $this->getDomain()), '.');
}
}
Налаштування Action
В GraphQL точка входу завжди залишається стабільно не змінною. За замовчуванням URL має вигляд /graphql
і до нього не додаються більше жодні параметри.
Створимо Action на який будуть спрямовуватись всі API запити, він водночас буде генерувати Schema
і повертати дані.
namespace Stagem\ZfcGraphQL\Action\Admin;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
//use Psr\Http\Server\MiddlewareInterface;
//use Psr\Http\Server\RequestHandlerInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Interop\Http\Server\RequestHandlerInterface;
use Fig\Http\Message\RequestMethodInterface;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\ServiceManager\ServiceManager;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Server\StandardServer;
use GraphQL\Doctrine\DefaultFieldResolver;
use GraphQL\Doctrine\Types;
use Stagem\Amazon\Model\Marketplace;
class IndexAction implements MiddlewareInterface, RequestMethodInterface
{
/**
* @var ServiceManager
*/
protected $container;
/**
* @var EntityManager
*/
protected $entityManager;
public function __construct(ContainerInterface $container, EntityManager $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
/** @var \Doctrine\ORM\Configuration $doctrineConfig */
// @todo remove when will be fixed @see https://github.com/Ecodev/graphql-doctrine/issues/21#issuecomment-432064584
$doctrineConfig = $this->container->get('doctrine.configuration.orm_default');
$doctrineConfig->getMetadataDriverImpl()
->setDefaultDriver(new \Doctrine\ORM\Mapping\Driver\AnnotationDriver(
new \Doctrine\Common\Annotations\AnnotationReader()
));
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
#$this->container->setAllowOverride(true);
#$this->container->setInvokableClass(DateTime::class, DateTimeType::class);
#$this->container->setAllowOverride(false);
// Configure the type registry
$types = new Types($this->entityManager, $this->container);
// Configure default field resolver to be able to use getters
GraphQL::setDefaultFieldResolver(new DefaultFieldResolver());
try {
$queryType = new ObjectType([
'name' => 'query', // @todo Try change to Query
'fields' => [
'marketplace' => [
'type' => $types->getOutput(Marketplace::class), // Use automated ObjectType for output
'description' => 'Returns marketplace by id (in range of 1-6)',
'args' => [
'id' => Type::nonNull(Type::id())
],
'resolve' => function ($root, $args) use ($types) {
$queryBuilder = $types->createFilteredQueryBuilder(Marketplace::class, $args['filter'] ?? [], $args['sorting'] ?? []);
$result = $queryBuilder->getQuery()->getArrayResult();
return $result;
},
],
'marketplaces' => [
'type' => Type::listOf($types->getOutput(Marketplace::class)), // Use automated ObjectType for output
'args' => [
[
'name' => 'filter',
'type' => $types->getFilter(Marketplace::class), // Use automated filtering options
],
[
'name' => 'sorting',
'type' => $types->getSorting(Marketplace::class), // Use automated sorting options
],
],
'resolve' => function ($root, $args) use ($types) {
$queryBuilder = $types->createFilteredQueryBuilder(Marketplace::class, $args['filter'] ?? [], $args['sorting'] ?? []);
$result = $queryBuilder->getQuery()->getArrayResult();
return $result;
},
],
],
'resolveField' => function($val, $args, $context, ResolveInfo $info) {
return $this->{$info->fieldName}($val, $args, $context, $info);
}
]);
/*$mutationType = new ObjectType([
'name' => 'mutation',
'fields' => [
'createMarketplace' => [
'type' => Type::nonNull($types->getOutput(Marketplace::class)),
'args' => [
'input' => Type::nonNull($types->getInput(Marketplace::class)), // Use automated InputObjectType for input
],
'resolve' => function ($root, $args): void {
// create new post and flush...
},
],
'updateMarketplace' => [
'type' => Type::nonNull($types->getOutput(Marketplace::class)),
'args' => [
'id' => Type::nonNull(Type::id()), // Use standard API when needed
'input' => $types->getPartialInput(Post::class), // Use automated InputObjectType for partial input for updates
],
'resolve' => function ($root, $args): void {
// update existing post and flush...
},
],
],
]);*/
// See docs on schema options:
// http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
$schema = new Schema([
'query' => $queryType,
//'mutation' => $mutationType,
]);
$schema->assertValid();
// See docs on server options:
// http://webonyx.github.io/graphql-php/executing-queries/#server-configuration-options
$server = new StandardServer([
'schema' => $schema
]);
$server->handleRequest();
} catch (\Exception $e) {
StandardServer::send500Error($e);
}
return new EmptyResponse();
}
}
Mutation
закоментовано свідомо щоб спрости реалізацію і не розсіювати увагу.
Власне це все, у $queryType['fields']
було додано два поля marketplace та marketplaces які повернуть Schema
для нашої моделі Marketplace
.
В список попорядку додаються нові моделі або за потреби пишеться скрипт який зробить це автоматично.
Робота з GraphiQL
Для тестування API GraphQL пропонує веб-інтерфейс GraphiQL, щоб його налаштувати, потрібно трішки повозитись. Зручніше на перший випадок використовувати додаток браузера ChromeiQL або будь який інший, всі вони мають схожий інтерфейс і виконують тіж самі функції.
Встановлюємо, відкриваємо, встановлюємо точку входу в наше API, наприклад http://localhost/admin/graphql, тиснемо Set Endpoint.
Виконуємо перший запит. З клієнтської сторони виглядає все дуже зручно й прозоро
{
marketplaces {
id
code
name
domain
}
}
У відповідь мають прийти ваші дані з бази даних. Щось на кшталт:
{
"data": {
"marketplaces": [
{
"id": "1",
"name": "DE",
"domain": "www.amazon.de"
},
{
"id": "2",
"name": "FR",
"domain": "www.amazon.fr"
},
{
"id": "3",
"name": "IT",
"domain": "www.amazon.it"
}
]
}
}
На правій панелі можна переглянути нашу схему, клікаючи на пункти буде відбуватись перехід на дочірні елементи.
Далі перевіримо фільтрацію і переконаємось, що все працює справно.
Для цього наш запит потрібно огорнути в конструкцію query
і надати йому ім’я. Запит буде мати такий вигляд:
query GetMarketplaces($filter: MarketplaceFilter) {
marketplaces(filter: $filter) {
id
name
domain
}
}
В Query Variables вставляємо тіло запиту. Конструкція доволі гроіздка але універсальна.
{
"filter": {
"groups": [{
"conditions": [{
"code": {
"equal": {
"value": "A13V1IB3VIYZZH"
}
}
}]
}]
}
}
Отримуємо результат:
{
"data": {
"marketplaces": [
{
"id": "2",
"name": "FR",
"domain": "www.amazon.fr"
}
]
}
}
На почитати
Thinking in Graphs
Awesome list of GraphQL & Relay
Webonyx GraphQL PHP documentation
Відео – GraphQL Introduction (укр.)
Відео – GraphQL Basic, Apollo (укр.)