GraphQL en Zend Expressive 3

Voy a intentar hacer una guia de paso a paso para utilizar GraphQL en Zend Expressive 3. Existe algo de documentación por la red pero, o está incompleta o desactualizada, por eso me he decidido a hacer este manual.

¿Que vamos a utilizar?


Otro día hablaré del por qué Zend Expressive.

¿Comenzamos?

Lo primero es crear un proyecto

$ composer create-project zendframework/zend-expressive-skeleton graphql-expressive

Recuerda que el directorio graphql-expressive debe estar vacío.

Ahora añadimos las dependencias para GraphQL

$ cd graphql-expressive
$ composer require webonyx/graphql-php

Ahora vamos con el código, lo primero que haremos será configurar un middleware para procesar correctamente las peticiones en JSON, editamos el archivo config/pipeline.php agregando estas dos líneas:
use Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware;
...
$app->pipe(BodyParamsMiddleware::class);

Primero vamos a crear una clase de registro para los tipos asociados a nuestras consultas, de esta forma nos será más fácil y práctico dividir por objetos las diferentes consultas a nuestra API, yo he decidido ubicarlo en src/App/src/GraphQL con el nombre de TypeRegistry.php y el siguiente contenido:
namespace App\GraphQL;

use GraphQL\Type\Definition\ObjectType;

class TypeRegistry
{
    public function get(string $name): ObjectType
    {
        $className = self::getClassName($name);
        return new $className($this);
    }

    protected static function getClassName(string $name): string
    {
        return __NAMESPACE__ . '\\Type\\' . ucfirst($name) . 'Type';
    }
}

No entraré en mucho detalle con los tipos pero del código anterior se deduce que estarán ubicados en el namespace App\GraphQL\Type y que la forma de llamarlos será invocando la función get del registro, si el tipo se llama BlogStoryType la llamada sería get('blogStory'), esta implementación es libre, cada uno la puede hacer cómo mejor le parezca.

Una vez creado el registro y antes de crear la ruta necesitamos especificar el manejador, que lo ubicaremos en src/App/src/Handler con el nombre de GraphQLHandler.php y el siguiente contenido:
namespace App\Handler;

use App\GraphQL\TypeRegistry;
use GraphQL\Server\StandardServer;
use GraphQL\Type\Schema;
use GraphQL\Type\SchemaConfig;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\JsonResponse;

class GraphQLHandler implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $typeRegistry = new TypeRegistry();
        $config = SchemaConfig::create()
            ->setQuery($typeRegistry->get('query'));
        $schema = new Schema($config);
        $server = new StandardServer([
            'schema' => $schema,
            'queryBatching' => true,
            'debug' => true
        ]);
        $response = $server->executePsrRequest($request);
        return new JsonResponse($response);
    }
}

Ahora configuramos la ruta en la que recibiremos las peticiones, en mi caso graphql (puede ser cualquiera) y solo admito peticiones por POST, esto ya es a gusto de cada uno.
$app->post('/graphql', App\Handler\GraphQLHandler::class, 'graphql');

Veamos un ejemplo
src/App/src/GraphQL/Type/UserType.php
namespace App\GraphQL\Type;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class UserType extends ObjectType
{
    public function __construct()
    {
        parent::__construct([
            'fields' => [
                'id' => Type::int(),
                'name' => Type::string()
            ]
        ]);
    }
}

src/App/src/GraphQL/Type/BlogStoryType.php
namespace App\GraphQL\Type;

use App\GraphQL\TypeRegistry;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

class BlogStoryType extends ObjectType
{
    public function __construct(TypeRegistry $types)
    {
        parent::__construct([
            'fields' => [
                'author' => [
                    'type' => $types->get('user'),
                    'resolve' => function (array $blogStory) {
                        $users = [
                            1 => [
                                'id' => 1,
                                'name' => 'Smith'
                            ],
                            2 => [
                                'id' => 2,
                                'name' => 'Anderson'
                            ]
                        ];
                        return $users[$blogStory['authorId']];
                    }
                ],
                'title' => Type::string()
            ]
        ]);
    }
}

src/App/src/GraphQL/Type/QueryType.php
namespace App\GraphQL\Type;

use App\GraphQL\TypeRegistry;
use GraphQL\Type\Definition\ObjectType;

class QueryType extends ObjectType
{
    public function __construct(TypeRegistry $types)
    {
        parent::__construct([
            'fields' => [
                'lastStory' => [
                    'type' => $types->get('blogStory'),
                    'resolve' => function () {
                        return [
                            'id' => 1,
                            'title' => 'Example blog post',
                            'authorId' => 1
                        ];
                    }
                ]
            ]
        ]);
    }
}

Y lo podemos ejecutar por dos vías, o por un script
$query = <<<'QL'
{
  lastStory {
    title
    author {
      name
    }
  }
}
QL;

$chObj = curl_init();
curl_setopt($chObj, CURLOPT_URL, 'http://graphql.lcl/graphql');
curl_setopt($chObj, CURLOPT_RETURNTRANSFER, true);
curl_setopt($chObj, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($chObj, CURLOPT_HEADER, true);
curl_setopt($chObj, CURLOPT_VERBOSE, true);
curl_setopt($chObj, CURLOPT_POSTFIELDS, $query);
curl_setopt($chObj, CURLOPT_HTTPHEADER,
    [
        'User-Agent: PHP Script',
        'Content-Type: application/graphql;charset=utf-8'
    ]
);

$response = curl_exec($chObj);
echo $response;

O utilizando la extensión para Firefox o Chrome de Altair GraphQL Client



Es importante saber el content type con el que vamos a hacer nuestras consultas, este método soporta ambos: application/graphql y application/json, la extensión para el navegador utiliza application/json y el ejemplo que os he puesto con curl utiliza application/graphql.

Comentarios