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 valoresnull; GraphQL lanzará error si el resolver devuelvenull.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::listOfsinnonNullsi 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).
