Si has estado al día, sabrás que he estado escribiendo una especie de mega serie sobre el desarrollo con GraphQL y Couchbase. En tutoriales anteriores hemos explorado cómo utilizar GraphQL con Java, cómo utilizar GraphQL con Node.jsy cómo utilizar GraphQL con Golang.
Después de haber producido contenido en torno a todos esos lenguajes, alguien de mi equipo me preguntó qué lugar ocupaba PHP en la mezcla. De ese comentario surgió este tutorial sobre la creación de una API con GraphQL y PHP usando Couchbase NoSQL como capa de datos.
Antes de invertir demasiado en este tutorial de GraphQL PHP y NoSQL, hay algunas suposiciones que deben cumplirse:
- Couchbase debe estar ya instalado y configurado.
- PHP debe estar instalado y configurado.
- Composer y PECL deben estar disponibles para descargar las dependencias del proyecto PHP.
Suponiendo que se cumplen las condiciones anteriores, podemos proceder a desarrollar nuestra aplicación.
Creación de un nuevo proyecto con dependencias GraphQL y Couchbase para PHP
El primer paso hacia el éxito de nuestro proyecto es crear uno y descargar las dependencias necesarias. En algún lugar de tu ordenador, crea un nuevo directorio e incluye un archivo graphql.php archivo. Utilizando su símbolo del sistema dentro de este directorio, ejecute lo siguiente:
1 |
compositor requiere webonyx/graphql-php |
El comando anterior instalará nuestra dependencia GraphQL que hace todo el trabajo pesado para nuestra API. A continuación, ejecute el siguiente comando desde el símbolo del sistema:
1 |
pecl instale couchbase |
El comando anterior instalará el SDK PHP de Couchbase. Reiterando mis suposiciones anteriores, necesitas tener Compositor y PECL a su disposición.
Más información SDK PHP de Couchbase y el Paquete GraphQL pueden encontrarse en cada una de sus respectivas documentaciones.
Configuración del proyecto para el desarrollo de GraphQL y Couchbase
Con el proyecto creado y las dependencias descargadas, podemos trabajar para arrancar nuestra aplicación. Abra el archivo graphql.php e incluya lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php require_once __DIR__ . /vendedor/autoload.php; utilice GraphQL\Tipo\Definición\Tipo de objeto; utilice GraphQL\Tipo\Definición\Tipo; utilice GraphQL\GraphQL; utilice GraphQL\Tipo\Esquema; $cluster = nuevo CouchbaseCluster("couchbase://localhost"); $authenticator = nuevo \Couchbase\PasswordAuthenticator(); $authenticator->nombre de usuario(ejemplo)->contraseña('123456'); $cluster->autentifique($authenticator); $bucket = $cluster->openBucket(ejemplo); cabecera(Content-Type: application/json'); ?> |
En el código anterior estamos haciendo referencia a nuestra dependencia Composer e incluyendo algunas de las clases GraphQL que planeamos utilizar. Sin llegar a configurar GraphQL o utilizar nuestra base de datos, necesitamos establecer una conexión con nuestra base de datos.
Mi aplicación PHP se ejecutaba localmente junto a mi instancia de Couchbase. Asegúrate de rellenar los huecos cuando se trate de tu propia información de conexión a Couchbase.
Definición de objetos GraphQL para la gestión de datos
Ahora podemos centrarnos en definir nuestro modelo de datos, también conocido como objetos GraphQL dentro de nuestra API. Para este ejemplo en particular vamos a hacer referencia a los datos de Pokemon. Por esta razón podríamos tener información sobre un Pokemon en particular, qué movimientos tiene, o en qué videojuego se encuentra. Esta será la base de nuestro modelo de datos.
Empecemos con lo siguiente en nuestro graphql.php utilizando este ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$moveType = nuevo Tipo de objeto([ nombre => Muévete, campos => [ nombre => [ tipo => Tipo::cadena() ], tipo => [ tipo => Tipo::cadena() ], poder => [ tipo => Tipo::int() ], ], ]); $pokemonTipo = nuevo Tipo de objeto([ nombre => Pokemon, campos => [ nombre => [ tipo => Tipo::cadena() ], "peso => [ tipo => Tipo::int() ], altura => [ tipo => Tipo::int() ], atributos => [ tipo => Tipo::listOf(Tipo::cadena()) ], movimientos => [ tipo => Tipo::listOf($moveType) ], ], ]); |
En el código anterior tenemos dos objetos con las posibles propiedades JSON, también llamadas campos. Para el $pokemonTipo
tenemos un campo que hace referencia a nuestro otro objeto. La dirección listOf
significa que podemos esperar que se devuelva un array de datos.
Los dos objetos que tenemos distan mucho de ser todo lo complejos que podrían ser, pero sirven de ejemplo realista. Sin embargo, por ahora no son más que un modelo. Necesitamos poder rellenarlos con datos reales de nuestra base de datos.
Desarrollo de consultas para rellenar objetos GraphQL
El siguiente paso es idear las consultas a ejecutar contra nuestra base de datos que llenarán nuestros objetos GraphQL. Esto se hace a través de consultas, que al fin y al cabo no es más que otro objeto GraphQL que contiene lógica.
Tomemos como ejemplo el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$queryType = nuevo Tipo de objeto([ nombre => Consulta, campos => [ pokemons => [ tipo => Tipo::listOf($pokemonTipo), "resolver => función ($raíz, $args) { global $cubo; $consulta = CouchbaseN1qlQuery::fromString("SELECT ejemplo.* FROM ejemplo WHERE tipo = 'pokemon'"); $resultado = $cubo->consulta($consulta); devolver $resultado->filas; } ], pokemon => [ tipo => $pokemonTipo, args => [ id => Tipo::nonNull(Tipo::cadena()), ], "resolver => función ($raíz, $args) { global $cubo; $resultado = $cubo->consiga("pokemon-" . $args[id]); devolver $resultado->valor; } ], ], ]); |
El código anterior, que terminaría en su graphql.php tiene dos posibilidades de consulta. La primera consulta se llama pokemons
devolverá una matriz con los valores $pokemonTipo
modelo. El sitio resolver
es lo que realmente ejecuta la lógica. La lógica para pokemons
ejecutará una consulta N1QL en Couchbase y devolverá el resultado. El resultado de la consulta N1QL se asigna a cada uno de los campos definidos en nuestro objeto GraphQL.
Ejecución de la pokemons
podría ser algo parecido a esto:
1 |
rizo http://localhost:8080 -d '{"query": "query { pokemons{ nombre, altura, atributos } }" }' |
En pokemon
es similar, pero no igual. En la pokemon
requerimos una id
se apruebe. Este id
se utiliza para realizar una búsqueda directa de un documento NoSQL concreto en lugar de una consulta NoSQL.
En pokemon
podría ser algo parecido a esto:
1 |
rizo http://localhost:8080 -d '{"query": "query { pokemon(id: \"25\"){ nombre, altura, atributos } }" }' |
El resultado de esta consulta será un único objeto en lugar de una matriz.
Manejo simplista de las relaciones entre modelos
Ahora digamos que queremos algún tipo de relación de datos. Hasta ahora hemos asumido que los datos de movimiento y los datos Pokemon existen en el mismo documento, pero ¿qué pasaría si tuviéramos datos que estuvieran divididos, pero relacionados?
Existen dos enfoques al respecto:
- Podemos hacer una consulta JOIN haciendo consultas potencialmente más complejas de gestionar.
- Podemos modular las consultas por campos para que sean pequeñas y sencillas.
Si has tenido la oportunidad de escuchar mi podcast titulado GraphQL para el desarrollo de API en el que aparecía el co-creador de GraphQL Lee Byron, sabrás que ninguna de las dos es errónea y que todo se reduce a las preferencias. Echemos un vistazo a esta última, en la que modularizamos las consultas y los objetos GraphQL.
Digamos que ahora queremos incluir datos del juego y es un objeto separado como este:
1 2 3 4 5 6 |
$gameType = nuevo Tipo de objeto([ nombre => Juego, campos => [ nombre => [ tipo => Tipo::cadena() ], ], ]); |
Ahora digamos que nuestros documentos Pokemon en nuestra base de datos contienen un id de juego, pero no los datos reales. Similar a una relación de clave primaria y foránea, pero no realmente porque esto es NoSQL.
Lo que podríamos hacer ahora es lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$pokemonTipo = nuevo Tipo de objeto([ nombre => Pokemon, campos => [ nombre => [ tipo => Tipo::cadena() ], "peso => [ tipo => Tipo::int() ], altura => [ tipo => Tipo::int() ], atributos => [ tipo => Tipo::listOf(Tipo::cadena()) ], movimientos => [ tipo => Tipo::listOf($moveType) ], juego => [ tipo => $gameType, "resolver => función ($raíz, $args) { global $cubo; $resultado = $cubo->consiga($raíz->juego); devolver $resultado->valor; } ], ], ]); |
Observe que hemos cambiado nuestro $pokemonTipo
para incluir un campo de juego, pero este campo tiene su propio resolver
función. Dentro de esta resolver
podemos acceder a la función padre o $root
que contiene un identificador de juego, porque eso es lo que nuestro pokemon
y pokemons
nos dio. Usando el id, podemos obtener los datos del juego que existen en un documento separado y devolverlos.
Ahora nuestro pokemons
se salva de una sentencia JOIN. No estoy diciendo que una sentencia JOIN sea mala, pero imagínese si tuviera algunos modelos de datos salvajes que requirieran una consulta de 50 líneas. En esos casos podría tener sentido dividirla en lugar de intentar mantener una consulta masiva. Como dato curioso, la operación de juego en la base de datos no se llamará a menos que el campo de juego se solicite en una sentencia Consulta GraphQL desde el frontend.
Integración de la aplicación
Es hora de unir la aplicación. Hasta ahora solo hemos visto los modelos de datos y las consultas, pero no la hemos configurado para usarla con GraphQL.
Eche un vistazo al código ahora completo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
<?php require_once __DIR__ . /vendedor/autoload.php; utilice GraphQL\Tipo\Definición\Tipo de objeto; utilice GraphQL\Tipo\Definición\Tipo; utilice GraphQL\GraphQL; utilice GraphQL\Tipo\Esquema; $cluster = nuevo CouchbaseCluster("couchbase://localhost"); $authenticator = nuevo \Couchbase\PasswordAuthenticator(); $authenticator->nombre de usuario(ejemplo)->contraseña('123456'); $cluster->autentifique($authenticator); $bucket = $cluster->openBucket(ejemplo); $gameType = nuevo Tipo de objeto([ nombre => Juego, campos => [ nombre => [ tipo => Tipo::cadena() ], ], ]); $moveType = nuevo Tipo de objeto([ nombre => Muévete, campos => [ nombre => [ tipo => Tipo::cadena() ], tipo => [ tipo => Tipo::cadena() ], poder => [ tipo => Tipo::int() ], ], ]); $pokemonTipo = nuevo Tipo de objeto([ nombre => Pokemon, campos => [ nombre => [ tipo => Tipo::cadena() ], "peso => [ tipo => Tipo::int() ], altura => [ tipo => Tipo::int() ], atributos => [ tipo => Tipo::listOf(Tipo::cadena()) ], movimientos => [ tipo => Tipo::listOf($moveType) ], juego => [ tipo => $gameType, "resolver => función ($root, $args) { global $bucket; 1TP4Resultado = $bucket->consiga($root->juego); devolver 1TP4Resultado->valor; } ], ], ]); $queryType = nuevo Tipo de objeto([ nombre => Consulta, campos => [ pokemons => [ tipo => Tipo::listOf($pokemonTipo), "resolver => función ($root, $args) { global $bucket; $query = CouchbaseN1qlQuery::fromString("SELECT ejemplo.* FROM ejemplo WHERE tipo = 'pokemon'"); 1TP4Resultado = $bucket->consulta($query); devolver 1TP4Resultado->filas; } ], pokemon => [ tipo => $pokemonTipo, args => [ id => Tipo::nonNull(Tipo::cadena()), ], "resolver => función ($root, $args) { global $bucket; 1TP4Resultado = $bucket->consiga("pokemon-" . $args[id]); devolver 1TP4Resultado->valor; } ], ], ]); 1TP4Esquema = nuevo Esquema([ consulta => $queryType ]); $rawInput = file_get_contents(php://entrada); 1TP4Entrada = json_decode($rawInput, verdadero); $query = 1TP4Entrada[consulta]; $variableValues = isset(1TP4Entrada[variables]) ? 1TP4Entrada[variables] : null; pruebe { 1TP4Resultado = GraphQL::executeQuery(1TP4Esquema, $query, null, null, $variableValues); 1TP4Salida = 1TP4Resultado->toArray(); } captura (\Excepción $e) { 1TP4Salida = [ errores => [ [ mensaje => $e->getMessage() ] ] ]; } cabecera(Content-Type: application/json'); echo json_encode(1TP4Salida); ?> |
La mitad inferior del código se ha extraído del archivo Documentación oficial del servidor PHP GraphQL. Esencialmente, estamos definiendo nuestras consultas como el esquema y tomando cualquier entrada del usuario final. Esta entrada se utiliza en combinación con el esquema para producir un resultado que se devuelve al usuario.
Conclusión
Acaba de ver cómo crear una API GraphQL con PHP y Couchbase como base de datos NoSQL. GraphQL es una gran alternativa a escribir APIs RESTful porque permite mantener adecuadamente un modelo de datos que puede ser consultado a través de un frontend. Esto reduce la cantidad de solicitudes HTTP, así como la carga útil devuelta en cualquier solicitud.
Si desea obtener más información sobre el uso de Couchbase con PHP, echa un vistazo a un tutorial anterior que escribí titulado, Primeros pasos con NoSQL usando Couchbase Server y PHP.