Blog

💁🏽‍♂️ Por que para suportar o CMS-agnosticism, o Gato GraphQL foi dividido em ~90 packages, e vantagens e desvantagens dessa abordagem

Leonardo Losoviz
Por Leonardo Losoviz ·

Na semana passada publiquei o artigo 💁🏻‍♀️ Por que o Gato GraphQL precisa de um monorepo, e como ele é otimizado, explicando como e por que o monorepo GatoGraphQL/GatoGraphQL, que hospeda o código do Gato GraphQL, consegue gerenciar o codebase do plugin de forma eficiente.

Compartilhei meu artigo no Reddit e recebi o seguinte comentário:

O artigo do OP e os artigos que ele referencia parecem tratar o monorepo como a maior invenção desde o pão fatiado.

Um artigo mais interessante seria explicar por que você achou que o CMS-agnosticism exige dividir tudo em seus próprios pequenos packages, e por que você achou que cada um dos mais de 200 packages precisava estar em seu próprio repositório desde o início.

É uma pergunta interessante. Então decidi escrever este artigo para respondê-la com um pouco mais de profundidade.

Mas antes, vou abordar dois tópicos relacionados: quantos packages são realmente necessários pelo plugin, e por que afirmo que o servidor GraphQL subjacente é CMS-agnostic.

Quantos packages compõem o plugin

Embora eu tenha mencionado mais de 200 packages PHP, isso se refere ao monorepo; para o plugin, são muito menos do que isso.

O monorepo GatoGraphQL/GatoGraphQL abrange 5 projetos:

  1. PoP, uma biblioteca de modelo de componentes server-side (como React, mas para o back-end)
  2. GraphQL by PoP, um servidor GraphQL CMS-agnostic para PHP
  3. Gato GraphQL
  4. um construtor de sites (WIP)
  5. Wassup, um tema de site baseado no construtor de sites (WIP)

Hospedar esses projetos em um monorepo simplifica o trabalho com eles, por causa de suas interdependências:

  • GraphQL by PoP é baseado em PoP
  • Gato GraphQL é baseado em GraphQL by PoP
  • O construtor de sites usa a biblioteca de modelo de componentes como seu motor (semelhante ao Gatsby usando GraphQL)
  • Wassup é baseado no construtor de sites

É em relação ao código de todos os 5 projetos que o GatoGraphQL/GatoGraphQL contém mais de 200 packages PHP. Em relação ao Gato GraphQL, são "apenas" 91 packages. E o GraphQL by PoP, o servidor GraphQL subjacente, contém "apenas" 98 packages.

(O plugin Gato GraphQL requer menos packages do que seu servidor GraphQL subjacente, porque alguns packages, como a diretiva @strTranslate do Google Translate, ainda não foram adicionados ao plugin.)

Como o GraphQL by PoP é CMS-agnostic? Como ele é diferente do webonyx?

Tenho dito que GraphQL by PoP é CMS-agnostic. Mas o que isso significa?

Aliás, o webonyx/graphql-php também é CMS-agnostic. Então, em que eles são diferentes?

O webonyx/graphql-php é CMS-agnostic no sentido de que é um package distribuído via Composer, contendo apenas código PHP "vanilla". No entanto, não é um servidor GraphQL por si só; em vez disso, é uma implementação em PHP da especificação GraphQL, para ser incorporada dentro de algum servidor GraphQL em PHP.

Agora, esses servidores GraphQL que o implementam, como Lighthouse ou WPGraphQL, não são CMS-agnostic. Não podemos rodar o Lighthouse no WordPress, nem o WPGraphQL no Laravel.

É nesse sentido que o GraphQL by PoP é CMS-agnostic: é o servidor GraphQL "quase-final", quase pronto para rodar com qualquer CMS ou framework, seja Laravel, WordPress ou qualquer outro. (Por brevidade, daqui em diante, sempre que eu disser "CMS", significa "CMS ou framework".)

Para torná-lo final para um determinado CMS, o servidor GraphQL ainda precisará de algum código personalizado para aquele CMS, por meio de um package correspondente.

Vou agora responder às perguntas do comentário.

Por que cada package precisava estar em seu próprio repositório

Porque o Packagist (o registro de packages PHP do Composer) exige que se forneça uma URL de repositório para publicar/distribuir um package.

(A propósito, meu artigo Hosting all your PHP packages together in a monorepo, também publicado na semana passada, fala sobre esse problema.)

Por que o CMS-agnosticism exige dividir tudo em seus próprios pequenos packages

Há algumas razões.

Fazer o CMS injetar seu próprio código

É impossível criar um servidor GraphQL que funcione em todo lugar, usando 100% o mesmo código PHP.

Por exemplo, para permitir que qualquer trecho de código modifique o valor de alguma variável em outro lugar, o WordPress depende dos filter hooks, o Symfony usa o componente EventDispatcher, e o Laravel tem seu próprio sistema de eventos e listeners. O código PHP para esses 3 métodos diferentes também será diferente.

É aqui que entra a abordagem de dividir o código em packages granulares. Em vez de ter uma solução para eventos e listeners como parte da aplicação, ela é injetada na aplicação via um package, e esse package conterá código específico para o CMS.

Para que isso funcione, cada funcionalidade deve ser dividida em 2 packages:

  • um package CMS-agnostic, contendo toda a lógica de negócio, usando apenas código PHP "vanilla". Este package incluirá os contratos a serem satisfeitos pelo package específico do CMS
  • um package específico do CMS, satisfazendo os contratos para aquele CMS

Por exemplo, o GraphQL by PoP tem um package hooks contendo o seguinte 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;
}

E então, o package hooks-wp satisfaz o 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);
  }
}

Agora, embora o conceito de hooks venha do WordPress, ele pode funcionar com outros CMSs também (por exemplo, usando eventos e listeners para implementar hooks). Então, podemos substituir hooks-wp por hooks-laravel, hooks-symfony, hooks-drupal, hooks-octobercms, ou qualquer outro, para satisfazer os contratos usando o código específico para cada CMS.

Permitir que o CMS descarte funcionalidades que não consegue suportar

Nem todos os CMSs conseguem suportar todas as funcionalidades. Por exemplo, o WordPress permite ordenar posts por alguma entrada meta_value, mas o OctoberCMS não.

É por isso que o GraphQL by PoP contém o package metaquery (satisfeito para WordPress via metaquery-wp). Assim, o servidor GraphQL implementado para WordPress incluirá este package, mas o destinado ao OctoberCMS não.

Vantagens dessa abordagem

Dividir nossos packages de forma granular oferece algumas vantagens.

Desacoplar a lógica de negócio do código específico do CMS

Em vez de codificar a aplicação com base na opinionatedness (forma de codificar, funcionalidades, limitações e outros aspectos) de um CMS, podemos abstrair nosso código e usar apenas a lógica de negócio.

Por exemplo, para obter uma lista de posts, a aplicação pode executar o método getPosts de alguma interface em um package CMS-agnostic posts. Assim, os posts serão sempre recuperados da mesma forma, independentemente da implementação pelo CMS subjacente.

Contornar a dívida técnica e usar os padrões mais recentes

Seguindo o exemplo acima, recuperamos nossos posts executando o método getPosts, que segue a convenção PSR-4, em vez de chamar get_posts, como definido pelo WordPress.

Da mesma forma, podemos executar getCustomPost para recuperar um custom post, em vez do impreciso get_post (isso faz parte da dívida técnica do WordPress).

É fácil de fazer scope

Usar o PHP-Scoper para fazer scope de um plugin WordPress não é fácil, e mesmo quando é possível, está sujeito a bugs.

Manter o código específico do CMS e a lógica de negócio da aplicação bem desacoplados permite aplicar o PHP-Scoper em apenas um conjunto de packages (os que contêm a lógica de negócio), e evitá-lo nos outros (os que contêm código WordPress). Descrevi essa estratégia em detalhes, aqui.

Além disso, assim como o PHP-Scoper, pode haver outras ferramentas que falham quando aplicadas a algum código específico de CMS (como o WordPress). Nesses casos, dividir os packages de forma granular pode salvar a situação.

Podemos produzir diferentes aplicações, cada uma contendo apenas o código de que precisa

Podemos reutilizar nossos packages para produzir mais aplicações, contendo apenas os packages de que cada uma precisa e nada mais.

Por exemplo, um blog pessoal pode precisar apenas de posts, tags e categories, podendo assim evitar lidar com funcionalidades para users ou user-login.

De fato, planejo me beneficiar dessa funcionalidade em breve: atualmente estou trabalhando na "Private GraphQL API", um motor GraphQL autônomo, a ser disponibilizado para desenvolvedores de plugins WordPress para incluí-lo em seus plugins, concedendo uma API GraphQL para seus blocos Gutenberg.

Posso criar a "Private GraphQL API" sem esforço simplesmente removendo do plugin Gato GraphQL os packages que não são necessários (os que lidam com UI, clients, custom endpoints, cache HTTP, queries persistidas e alguns outros).

Por fim, como é fácil de fazer scope (como visto acima), posso prefixar todos os packages necessários, de modo que a Private GraphQL API funcione sem conflitos (o que poderia ocorrer quando 2 plugins diferentes incluem versões diferentes da Private GraphQL API).

Desvantagens dessa abordagem

Escusado dizer que essa abordagem está longe de ser perfeita.

Maior esforço, o código se torna mais verboso

Normalmente, se nossa aplicação roda no WordPress, para recuperar uma lista de posts simplesmente executamos get_posts. Simples e fácil.

Torná-la CMS-agnostic complica significativamente as coisas. Para recuperar uma lista de posts, devemos:

  • Criar os packages posts e posts-wp
  • Criar um contrato com a função getPosts no package posts
  • Satisfazer o contrato via get_posts no package posts-wp
  • Sempre garantir que a funcionalidade seja invocada por meio do contrato, nunca diretamente

(Muito provavelmente) requer injeção de dependência

Precisamos vincular cada contrato do package CMS-agnostic e sua implementação do package específico do CMS. No meu caso, estou usando um container de serviços, fornecido pelo componente DependencyInjection do Symfony.

Adoro essa abordagem, acredito que simplifica muito a aplicação. No entanto, entendo que nem toda aplicação necessitaria de injeção de dependência de outra forma, adicionando complexidade a ela.

(Muito provavelmente) requer um monorepo

O Gato GraphQL acabou contendo 91 packages. No passado, eu hospedava cada um deles em seu próprio repositório, o que tornava muito difícil criar PRs. Então fui "forçado" a adotar a abordagem de monorepo.

Para ser claro: eu realmente gosto do monorepo. Mas entendo que nem todo mundo gosta, e ele também exige seu próprio esforço de manutenção.

Anteriormente escrevi sobre minhas motivações e estratégia para abstrair meu site WordPress, tornando-o CMS-agnostic. É essa mesma estratégia que apliquei para dividir o codebase do Gato GraphQL:

Adendo: Lista dos 91 packages que compõem o plugin

O Gato GraphQL contém os seguintes 91 packages.

Funcionalidades do 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

Funcionalidades de API:

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

Funcionalidades do 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 dados:

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

Assine nossa newsletter

Fique por dentro de todas as atualizações do Gato GraphQL.