Blog

💁🏽‍♂️ Por qué para soportar CMS-agnosticism, Gato GraphQL se dividió en ~90 paquetes, y beneficios e inconvenientes de este enfoque

Leonardo Losoviz
Por Leonardo Losoviz ·

La semana pasada publiqué el artículo 💁🏻‍♀️ Por qué Gato GraphQL necesita un monorepo, y cómo está optimizado, explicando cómo y por qué el monorepo GatoGraphQL/GatoGraphQL, que aloja el código de Gato GraphQL, puede gestionar el código base del plugin eficientemente.

Compartí mi artículo en Reddit, y recibí el siguiente comentario:

El artículo del OP y los artículos a los que enlaza, se leen como si un monorepo fuera la mejor cosa desde el pan rebanado.

Un artículo más interesante sería explicar por qué pensaste que el CMS-agnosticism requiere dividir todo en su propio pequeño paquete, y por qué pensaste que cada uno de los más de 200 paquetes necesitaba estar en su propio repo en primer lugar.

Esta es una pregunta interesante. Así que decidí escribir este artículo, para abordarla un poco más.

Pero primero, abordaré dos temas relacionados: cuántos paquetes son realmente requeridos por el plugin, y por qué afirmo que el servidor GraphQL subyacente es CMS-agnostic.

Cuántos paquetes componen el plugin

Aunque he mencionado más de 200 paquetes PHP, eso es para el monorepo; para el plugin, son en realidad muchos menos.

El monorepo GatoGraphQL/GatoGraphQL abarca 5 proyectos:

  1. PoP, una librería de modelo de componentes del lado del servidor (como React, pero para el back-end)
  2. GraphQL by PoP, un servidor GraphQL CMS-agnostic para PHP
  3. Gato GraphQL
  4. un site builder (WIP)
  5. Wassup, un theme de sitio web basado en el site builder (WIP)

Alojar estos proyectos en un monorepo simplifica trabajar con ellos, debido a sus interdependencias:

  • GraphQL by PoP se basa en PoP
  • Gato GraphQL se basa en GraphQL by PoP
  • El site builder usa la librería del modelo de componentes como su motor (similar a Gatsby usando GraphQL)
  • Wassup se basa en el site builder

Es respecto al código para los 5 proyectos que GatoGraphQL/GatoGraphQL contiene más de 200 paquetes PHP. Respecto a Gato GraphQL, son "solo" 91 paquetes. Y GraphQL by PoP, el servidor GraphQL subyacente, contiene "solo" 98 paquetes.

(El plugin Gato GraphQL requiere menos paquetes que su servidor GraphQL subyacente, porque algunos paquetes, como la directiva @strTranslate de Google Translate, aún no se han añadido al plugin.)

¿Cómo es GraphQL by PoP CMS-agnostic? ¿En qué se diferencia de webonyx?

He estado diciendo que GraphQL by PoP es CMS-agnostic. ¿Pero qué significa eso?

Para el caso, webonyx/graphql-php también es CMS-agnostic. Entonces, ¿en qué se diferencian?

webonyx/graphql-php es CMS-agnostic, en que es un paquete distribuido vía Composer, que contiene solo código PHP "vanilla". Sin embargo, no es un servidor GraphQL por sí mismo; en su lugar, es una implementación en PHP de la especificación GraphQL, para ser embebido dentro de algún servidor GraphQL en PHP.

Ahora, estos servidores GraphQL que lo implementan, como Lighthouse o WPGraphQL, no son CMS-agnostic. No podemos ejecutar Lighthouse en WordPress, o WPGraphQL en Laravel.

Es en este sentido que GraphQL by PoP es CMS-agnostic: es el servidor GraphQL "casi-final", casi listo para ejecutarse con cualquier CMS o framework, sea Laravel, WordPress, o cualquier otro. (Por brevedad, de ahora en adelante, cuando diga "CMS", significa "CMS o framework".)

Para hacerlo final para algún CMS, el servidor GraphQL aún necesitará algo de código personalizado para ese CMS, vía algún paquete correspondiente.

Ahora abordaré las preguntas del comentario.

Por qué cada paquete necesitaba estar en su propio repo

Porque Packagist (el registro de paquetes PHP de Composer) requiere proporcionar una URL de repositorio para publicar/distribuir un paquete.

(Por cierto, mi artículo Hosting all your PHP packages together in a monorepo, también publicado la semana pasada, habla de este problema.)

Por qué CMS-agnosticism requiere dividir todo en su propio pequeño paquete

Hay algunas razones.

Que el CMS inyecte su propio código

Es imposible hacer un servidor GraphQL que funcione en todas partes, usando el 100 % del mismo código PHP.

Por ejemplo, para permitir que cualquier pieza de código modifique el valor de alguna variable en otro lugar, WordPress se basa en filter hooks, Symfony usa el componente EventDispatcher, y Laravel tiene su propio sistema de eventos y listeners. El código PHP para estos 3 métodos diferentes también será diferente.

Aquí es donde entra el enfoque de dividir el código en paquetes granulares. En lugar de tener una solución para eventos y listeners como parte de la aplicación, se inyecta en la aplicación vía un paquete, y este paquete contendrá código que es específico del CMS.

Para que esto funcione, cada funcionalidad debe dividirse en 2 paquetes:

  • un paquete CMS-agnostic, que contiene toda la lógica de negocio, usando solo código PHP "vanilla". Este paquete incluirá los contratos a ser satisfechos por el paquete específico del CMS
  • un paquete específico del CMS, satisfaciendo los contratos para ese CMS

Por ejemplo, GraphQL by PoP tiene un paquete hooks que contiene el siguiente contrato:

interface HooksAPIInterface
{
  public function addFilter(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeFilter(string $tag, callable $function_to_remove, int $priority = 10): bool;
  public function applyFilters(string $tag, mixed $value, mixed ...$args): mixed;
  public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void;
  public function removeAction(string $tag, callable $function_to_remove, int $priority = 10): bool;
  public function doAction(string $tag, mixed ...$args): void;
}

Y entonces, el paquete hooks-wp satisface el contrato para WordPress:

class HooksAPI implements HooksAPIInterface
{
  public function addFilter(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    \add_filter($tag, $function_to_add, $priority, $accepted_args);
  }
  public function removeFilter(string $tag, callable $function_to_remove, int $priority = 10): bool
  {
    return \remove_filter($tag, $function_to_remove, $priority);
  }
  public function applyFilters(string $tag, mixed $value, mixed ...$args): mixed
  {
    return \apply_filters($tag, $value, ...$args);
  }
  public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
  {
    \add_action($tag, $function_to_add, $priority, $accepted_args);
  }
  public function removeAction(string $tag, callable $function_to_remove, int $priority = 10): bool
  {
    return \remove_action($tag, $function_to_remove, $priority);
  }
  public function doAction(string $tag, mixed ...$args): void
  {
    \do_action($tag, ...$args);
  }
}

Ahora, aunque el concepto de hooks viene de WordPress, puede funcionar también con otros CMSs (por ejemplo, usando eventos y listeners para implementar hooks). Entonces, podemos reemplazar hooks-wp con hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, o cualquier otro, para satisfacer los contratos usando el código específico de cada CMS.

Permitir al CMS descartar funcionalidad que no puede soportar

No todos los CMSs pueden soportar toda la funcionalidad. Por ejemplo, WordPress permite ordenar entradas por alguna entrada meta_value, pero OctoberCMS no.

Por eso GraphQL by PoP contiene el paquete metaquery (satisfecho para WordPress vía metaquery-wp). Entonces, el servidor GraphQL implementado para WordPress incluirá este paquete, pero el de OctoberCMS no.

Beneficios de este enfoque

Dividir nuestros paquetes granularmente ofrece algunas ventajas.

Desacoplar la lógica de negocio del código específico del CMS

En lugar de codificar la aplicación basándonos en la opinionatedness (forma de codificar, características, limitaciones, y otras) de un CMS, podemos abstraer nuestro código y usar solo lógica de negocio.

Por ejemplo, para obtener una lista de entradas, la aplicación puede ejecutar el método getPosts desde alguna interfaz en un paquete CMS-agnostic posts. Entonces, las entradas se recuperarán siempre de la misma forma, independientemente de la implementación del CMS subyacente.

Eludir la deuda técnica, y usar los últimos estándares

Siguiendo el ejemplo anterior, recuperamos nuestras entradas ejecutando el método getPosts, que sigue la convención PSR-4, en lugar de llamar a get_posts, como se define en WordPress.

De forma similar, podemos ejecutar getCustomPost para recuperar un custom post, en lugar del inexacto get_post (esto es parte de la deuda técnica de WordPress).

Es fácil hacer scoping

Usar PHP-Scoper para hacer scoping de un plugin WordPress no es fácil, e incluso cuando es factible, es propenso a bugs.

Mantener el código específico del CMS y la lógica de negocio de la aplicación bien desacoplados, permite aplicar PHP-Scoper en un solo conjunto de paquetes (los que tienen la lógica de negocio), y evitarlo en los otros (los que contienen código WordPress). He descrito esta estrategia en detalle, aquí.

Además, similar a PHP-Scoper, puede haber otras herramientas que fallen al aplicarse en algún código específico del CMS (como WordPress). En esos casos, dividir los paquetes granularmente puede salvar el día.

Podemos producir distintas aplicaciones, cada una con solo el código que necesita

Podemos reusar nuestros paquetes para producir más aplicaciones, conteniendo solo aquellos paquetes que necesita y nada más.

Por ejemplo, un blog personal puede necesitar solo posts, tags y categories, así que puede evitar tratar con funcionalidad para users o user-login.

De hecho, planeo beneficiarme de esta característica pronto: actualmente estoy trabajando en la "Private GraphQL API", un motor GraphQL autocontenido, para ser puesto a disposición de los desarrolladores de plugins de WordPress para empaquetarlo dentro de sus plugins, otorgando una API GraphQL para sus bloques Gutenberg.

Puedo crear sin esfuerzo la "Private GraphQL API" simplemente eliminando aquellos paquetes del plugin Gato GraphQL que no son necesarios (los que tratan con UI, clientes, custom endpoints, HTTP caching, persisted queries, y algunos otros).

Finalmente, como es fácil hacer scoping (como se vio arriba), puedo prefijar todos los paquetes requeridos, así que la Private GraphQL API funcionará sin conflicto (lo que podría ocurrir cuando 2 plugins distintos empaquetan distintas versiones de la Private GraphQL API).

Inconvenientes de este enfoque

Por supuesto, este enfoque está lejos de ser perfecto.

Mayor esfuerzo, el código se vuelve más verboso

Normalmente, si nuestra aplicación se ejecuta en WordPress, para recuperar una lista de entradas simplemente ejecutamos get_posts. Simple y fácil.

Hacerlo CMS-agnostic complica las cosas significativamente. Para recuperar una lista de entradas, debemos:

  • Crear los paquetes posts y posts-wp
  • Crear un contrato con la función getPosts en el paquete posts
  • Satisfacer el contrato vía get_posts en el paquete posts-wp
  • Asegurarnos siempre de invocar la funcionalidad vía el contrato, nunca directamente

(Muy probablemente) requiere inyección de dependencias

Necesitamos enlazar cada contrato del paquete CMS-agnostic, y su implementación del paquete específico del CMS. En mi caso, estoy usando un service container, proporcionado por el componente DependencyInjection de Symfony.

Me encanta este enfoque, creo que simplifica enormemente la aplicación. Sin embargo, entiendo que no todas las aplicaciones requerirían de otro modo inyección de dependencias, añadiendo complejidad a la misma.

(Lo más probable) requiere un monorepo

Gato GraphQL terminó conteniendo 91 paquetes. En el pasado, alojaba cada uno de ellos en su propio repositorio, lo que hacía muy difícil crear PRs. Así que me he visto "forzado" a cambiar al enfoque monorepo.

Para ser claro: realmente me gusta el monorepo. Pero entiendo que no a todo el mundo le gusta, y también requiere su propio esfuerzo para mantener.

Enlaces útiles

He escrito previamente sobre mis motivaciones y estrategia para abstraer mi sitio WordPress, haciéndolo CMS-agnostic. Es esta misma estrategia la que apliqué para dividir el código base de Gato GraphQL:

Addendum: Lista de los 91 paquetes que componen el plugin

Gato GraphQL contiene los siguientes 91 paquetes.

Funcionalidad del motor:

getpop/access-control
getpop/cache-control
getpop/component-model
getpop/definitions
getpop/engine
getpop/engine-wp
getpop/field-query
getpop/guzzle-helpers
getpop/hooks
getpop/hooks-wp
getpop/loosecontracts
getpop/mandatory-directives-by-configuration
getpop/modulerouting
getpop/query-parsing
getpop/root
getpop/routing
getpop/routing-wp
getpop/translation
getpop/translation-wp
graphql-api/markdown-convertor

Funcionalidad de API:

getpop/api
getpop/api-clients
getpop/api-endpoints
getpop/api-endpoints-for-wp
getpop/api-graphql
getpop/api-mirrorquery

Funcionalidad del servidor GraphQL:

graphql-by-pop/graphql-clients-for-wp
graphql-by-pop/graphql-endpoint-for-wp
graphql-by-pop/graphql-parser
graphql-by-pop/graphql-query
graphql-by-pop/graphql-request
graphql-by-pop/graphql-server

Modelo de datos:

pop-schema/basic-directives
pop-schema/categories
pop-schema/categories-wp
pop-schema/comment-mutations
pop-schema/comment-mutations-wp
pop-schema/commentmeta
pop-schema/commentmeta-wp
pop-schema/comments
pop-schema/comments-wp
pop-schema/custompost-mutations
pop-schema/custompost-mutations-wp
pop-schema/custompostmedia
pop-schema/custompostmedia-mutations
pop-schema/custompostmedia-mutations-wp
pop-schema/custompostmedia-wp
pop-schema/custompostmeta
pop-schema/custompostmeta-wp
pop-schema/customposts
pop-schema/customposts-wp
pop-schema/generic-customposts
pop-schema/media
pop-schema/media-wp
pop-schema/menus
pop-schema/menus-wp
pop-schema/meta
pop-schema/metaquery
pop-schema/metaquery-wp
pop-schema/pages
pop-schema/pages-wp
pop-schema/post-categories
pop-schema/post-categories-wp
pop-schema/post-mutations
pop-schema/post-tags
pop-schema/post-tags-wp
pop-schema/posts
pop-schema/posts-wp
pop-schema/queriedobject
pop-schema/queriedobject-wp
pop-schema/schema-commons
pop-schema/tags
pop-schema/tags-wp
pop-schema/taxonomies
pop-schema/taxonomies-wp
pop-schema/taxonomymeta
pop-schema/taxonomymeta-wp
pop-schema/taxonomyquery
pop-schema/taxonomyquery-wp
pop-schema/user-roles
pop-schema/user-roles-access-control
pop-schema/user-roles-wp
pop-schema/user-state
pop-schema/user-state-access-control
pop-schema/user-state-mutations
pop-schema/user-state-mutations-wp
pop-schema/user-state-wp
pop-schema/usermeta
pop-schema/usermeta-wp
pop-schema/users
pop-schema/users-wp

Suscríbete a nuestra newsletter

Mantente al tanto de todas las novedades de Gato GraphQL.