GraphQL en PHP con webonyx/graphql-php: schema, queries y mutations

GraphQL es un lenguaje de consulta para APIs que permite al cliente pedir exactamente los campos que necesita. A diferencia de REST, un único endpoint atiende todas las peticiones. En PHP, webonyx/graphql-php es la implementación de referencia.

Instalación

composer require webonyx/graphql-php

Definir un ObjectType

Cada entidad del dominio se representa como un ObjectType. Aquí el tipo Producto:

<?php
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;

$productoType = new ObjectType([
    'name' => 'Producto',
    'fields' => [
        'id'     => Type::nonNull(Type::int()),
        'nombre' => Type::nonNull(Type::string()),
        'precio' => Type::float(),
        'stock'  => Type::int(),
    ],
]);
?>

Query: el tipo raíz de consultas

<?php
$queryType = new ObjectType([
    'name' => 'Query',
    'fields' => [
        'producto' => [
            'type' => $productoType,
            'args' => [
                'id' => Type::nonNull(Type::int()),
            ],
            'resolve' => function ($root, array $args): ?array {
                // Aquí iría la consulta real a la BD
                $productos = [
                    1 => ['id' => 1, 'nombre' => 'Teclado', 'precio' => 49.99, 'stock' => 12],
                    2 => ['id' => 2, 'nombre' => 'Ratón',   'precio' => 29.99, 'stock' => 0],
                ];
                return $productos[$args['id']] ?? null;
            },
        ],
        'productos' => [
            'type'    => Type::listOf($productoType),
            'resolve' => fn($root, $args) => [
                ['id' => 1, 'nombre' => 'Teclado', 'precio' => 49.99, 'stock' => 12],
                ['id' => 2, 'nombre' => 'Ratón',   'precio' => 29.99, 'stock' => 0],
            ],
        ],
    ],
]);
?>

Construir el schema y ejecutar una consulta

<?php
use GraphQLGraphQL;
use GraphQLTypeSchema;

$schema = new Schema([
    'query' => $queryType,
]);

$query = '{ producto(id: 1) { nombre precio } }';

$resultado = GraphQL::executeQuery($schema, $query);
echo json_encode($resultado->toArray());
// {"data":{"producto":{"nombre":"Teclado","precio":49.99}}}
?>

Mutations: operaciones de escritura

Las mutations modifican datos. Se definen igual que las queries pero bajo el tipo raíz Mutation:

<?php
$mutationType = new ObjectType([
    'name' => 'Mutation',
    'fields' => [
        'crearProducto' => [
            'type' => $productoType,
            'args' => [
                'nombre' => Type::nonNull(Type::string()),
                'precio' => Type::nonNull(Type::float()),
            ],
            'resolve' => function ($root, array $args): array {
                // Aquí iría el INSERT en la BD
                return [
                    'id'     => rand(10, 99),
                    'nombre' => $args['nombre'],
                    'precio' => $args['precio'],
                    'stock'  => 0,
                ];
            },
        ],
    ],
]);

$schema = new Schema([
    'query'    => $queryType,
    'mutation' => $mutationType,
]);
?>

Llamada desde el cliente:

mutation {
  crearProducto(nombre: "Monitor", precio: 299.00) {
    id
    nombre
  }
}

Tipos de lista y non-null

  • Type::listOf($tipo): devuelve un array de elementos del tipo indicado.
  • Type::nonNull($tipo): prohíbe valores null; GraphQL lanzará error si el resolver devuelve null.
  • Type::nonNull(Type::listOf(Type::nonNull($tipo))): lista no nula de elementos no nulos.

Endpoint HTTP

<?php
// graphql.php
require 'vendor/autoload.php';

use GraphQLGraphQL;
use GraphQLTypeSchema;

header('Content-Type: application/json');

$input  = json_decode(file_get_contents('php://input'), true);
$query  = $input['query']     ?? '';
$vars   = $input['variables'] ?? null;

try {
    $resultado = GraphQL::executeQuery($schema, $query, null, null, $vars);
    echo json_encode($resultado->toArray());
} catch (Exception $e) {
    echo json_encode(['errors' => [['message' => $e->getMessage()]]]);
}
?>

Lazy loading de tipos

Cuando el schema crece, los tipos circulares (Producto ? Categoria ? Producto) darían error si se pasan como objetos creados en el momento. La solución es usar closures en el campo fields:

<?php
$categoriaType = new ObjectType([
    'name' => 'Categoria',
    'fields' => function () use (&$productoType): array {
        return [
            'id'        => Type::int(),
            'nombre'    => Type::string(),
            'productos' => Type::listOf($productoType),
        ];
    },
]);
?>

Errores frecuentes

  • «Cannot query field X on type Y»: el campo pedido en la consulta no existe en el ObjectType. Revisa el nombre exacto.
  • Resolver devuelve null en campo non-null: GraphQL propagará el null al nivel superior. Usa Type::listOf sin nonNull si los datos pueden no existir.
  • N+1 queries: cada resolver que lanza una consulta por registro genera el problema N+1. La solución es Dataloader (librería leinonen/php-dataloader).

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP