ブログ

💁🏽‍♂️ CMS非依存性をサポートするために、Gato GraphQLが約90のパッケージに分割された理由と、このアプローチのメリット・デメリット

Leonardo Losoviz
著者: Leonardo Losoviz ·

先週、💁🏻‍♀️ Gato GraphQLがなぜMonorepoを必要とするのか、そしてどのように最適化されているのかという記事を公開しました。Gato GraphQLのコードをホストしているGatoGraphQL/GatoGraphQLモノレポが、プラグインのコードベースをいかに効率よく管理できるかについて説明しています。

この記事をRedditでシェアしたところ、次のようなコメントをいただきました:

OPの記事とそこからリンクされている記事を読むと、モノレポはスライスされたパン以来最高のものであるかのように読めます。

より興味深い記事は、なぜCMS非依存性のためにすべてをそれぞれ独自のパッケージに分割する必要があると考えたのか、そしてなぜ200以上のパッケージそれぞれを最初から独自のリポジトリに置く必要があると考えたのかを説明するものでしょう。

これは興味深い質問です。そこでこの記事を書き、もう少し詳しく説明することにしました。

しかしまず、関連する2つのトピックを取り上げます。プラグインが実際に必要とするパッケージの数と、なぜ基盤となるGraphQLサーバーがCMS非依存であると主張するのかについてです。

プラグインを構成するパッケージの数

200以上のPHPパッケージに言及しましたが、それはモノレポに関するものです。プラグインに関しては、実際にはその数よりはるかに少ないです。

GatoGraphQL/GatoGraphQLモノレポは5つのプロジェクトを含んでいます:

  1. PoP:サーバーサイドのコンポーネントモデルライブラリ(Reactに似ていますが、バックエンド向け)
  2. GraphQL by PoP:PHP向けのCMS非依存なGraphQLサーバー
  3. Gato GraphQL
  4. サイトビルダー(WIP)
  5. Wassup:サイトビルダーをベースにしたウェブサイトテーマ(WIP)

これらのプロジェクトをモノレポでホストすることで、相互依存関係のために作業が簡素化されます:

  • GraphQL by PoPはPoPをベースにしています
  • Gato GraphQLはGraphQL by PoPをベースにしています
  • サイトビルダーはコンポーネントモデルライブラリをエンジンとして使用します(GatsbyがGraphQLを使用するのに似ています)
  • Wassupはサイトビルダーをベースにしています

5つのプロジェクトすべてのコードに関して、GatoGraphQL/GatoGraphQLは200以上のPHPパッケージを含んでいます。Gato GraphQLに関しては「わずか」91パッケージです。そして基盤となるGraphQLサーバーであるGraphQL by PoPは「わずか」98パッケージを含んでいます。

(Gato GraphQLプラグインが基盤となるGraphQLサーバーよりも少ないパッケージしか必要としないのは、Google Translateの@strTranslateディレクティブのような一部のパッケージがまだプラグインに追加されていないためです。)

GraphQL by PoPはどのようにCMS非依存なのか?webonyxとの違いは?

GraphQL by PoPはCMS非依存であると述べてきました。しかし、それはどういう意味なのでしょうか?

ちなみに、webonyx/graphql-phpもCMS非依存です。では両者の違いは何でしょうか?

webonyx/graphql-phpがCMS非依存なのは、Composerを通じて配布されるパッケージであり、「バニラ」PHPコードのみを含んでいるという意味においてです。ただし、それ自体では完全なGraphQLサーバーではなく、PHPにおけるGraphQL仕様の実装であり、PHPの何らかのGraphQLサーバーに組み込まれることを前提としています。

LighthouseWPGraphQLのようなこれらのGraphQLサーバーは、CMS非依存ではありません。LighthouseをWordPressで動かしたり、WPGraphQLをLaravelで動かすことはできません。

この意味においてGraphQL by PoPはCMS非依存です。つまり、Laravel、WordPress、またはその他あらゆるCMSやフレームワークで動作できる「ほぼ完成した」GraphQLサーバーなのです。(以降、簡潔にするため「CMS」は「CMSまたはフレームワーク」を意味するものとします。)

特定のCMS向けに完成させるには、そのCMS用のカスタムコードが、対応するパッケージを通じて依然として必要です。

次に、コメントの質問に回答します。

なぜ各パッケージを独自のリポジトリに置く必要があったのか

Packagist(ComposerのPHPパッケージレジストリ)がパッケージを公開・配布するためにリポジトリURLの提供を要求しているからです。

(ちなみに、先週も公開した記事Hosting all your PHP packages together in a monorepoでも、この問題について述べています。)

なぜCMS非依存性のためにすべてを独自の小さなパッケージに分割する必要があるのか

いくつかの理由があります。

CMSに独自のコードを注入させる

100%同じPHPコードを使用して、どこでも動作するGraphQLサーバーを作ることは不可能です。

例えば、コードの一部が別の場所にある変数の値を変更できるようにするために、WordPressはフィルターフックに依存し、SymfonyはEventDispatcherコンポーネントを使用し、Laravelには独自のイベントとリスナーのシステムがあります。これら3つの異なるメソッドのPHPコードも異なります。

ここで、コードを粒度の高いパッケージに分割するアプローチが登場します。イベントとリスナーのソリューションをアプリケーションの一部として持つ代わりに、パッケージを通じてアプリケーションに注入され、このパッケージにはCMS固有のコードが含まれます。

これを機能させるためには、すべての機能を2つのパッケージに分割する必要があります:

  • CMS非依存パッケージ:すべてのビジネスロジックを含み、「バニラ」PHPコードのみを使用します。このパッケージにはCMS固有パッケージが満たすべきコントラクトが含まれます
  • CMS固有パッケージ:そのCMSのコントラクトを満たします

例えば、GraphQL by PoPにはhooksパッケージがあり、次のコントラクトを含んでいます:

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;
}

そして、パッケージhooks-wpWordPressのコントラクトを満たします

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);
  }
}

フックのコンセプトはWordPressに由来しますが、他のCMSでも機能します(例えば、フックを実装するためにイベントとリスナーを使用する場合など)。そのため、hooks-wphooks-laravelhooks-symfonyhooks-drupalhooks-octobercms、またはその他のものに置き換えることで、各CMSに固有のコードを使用してコントラクトを満たすことができます。

CMSがサポートできない機能を破棄できるようにする

すべてのCMSがすべての機能をサポートできるわけではありません。例えば、WordPressではmeta_valueエントリによって投稿を並び替えることができますが、OctoberCMSではできません。

そのためGraphQL by PoPにはmetaqueryパッケージがあります(WordPress向けにはmetaquery-wpを通じて満たされます)。WordPress向けに実装されたGraphQLサーバーはこのパッケージを含みますが、OctoberCMS向けのものは含みません。

このアプローチのメリット

パッケージを粒度高く分割することで、いくつかの利点があります。

ビジネスロジックをCMS固有コードから切り離す

CMSの個性(コーディング方法、機能、制限など)に基づいてアプリケーションをコーディングする代わりに、コードを抽象化してビジネスロジックのみを使用できます。

例えば、投稿のリストを取得するために、アプリケーションはCMS非依存パッケージpostsのインターフェースからgetPostsメソッドを実行できます。そうすれば、基盤となるCMSによる実装に関係なく、投稿は常に同じ方法で取得されます。

技術的負債を回避し、最新の標準を使用する

上記の例に続き、PSR-4規約に従うgetPostsメソッドを実行して投稿を取得します。WordPressで定義されているget_postsを呼び出す代わりに。

同様に、不正確なget_postの代わりにgetCustomPostを実行してカスタム投稿を取得できます(これはWordPressの技術的負債の一部です)。

スコープ適用が容易

WordPressプラグインにPHP-Scoperを使用してスコープを適用することは容易ではなく、可能な場合でもバグが発生しやすいです。

CMS固有のコードとアプリケーションのビジネスロジックを徹底的に分離しておくことで、一部のパッケージ(ビジネスロジックを持つもの)にのみPHP-Scoperを適用し、他のパッケージ(WordPressコードを含むもの)には適用しないことができます。この戦略についてはこちらで詳しく説明しています。

さらに、PHP-Scoperと同様に、一部のCMS固有コード(WordPressなど)に適用すると失敗するツールが他にもある可能性があります。そのような場合、パッケージを粒度高く分割することで解決できます。

必要なコードのみを含む異なるアプリケーションを生成できる

パッケージを再利用して、必要なパッケージのみを含む複数のアプリケーションを生成できます。

例えば、個人ブログはpoststagscategoriesのみが必要であるため、usersuser-loginの機能を扱う必要がありません。

実際に、近いうちにこの機能を活用する予定です。現在「Private GraphQL API」の開発に取り組んでいます。これは自己完結型のGraphQLエンジンで、WordPressプラグイン開発者がプラグイン内にバンドルして、GutenbergブロックにGraphQL APIを提供できるようにするものです。

Gato GraphQLプラグインから不要なパッケージ(UI、クライアント、カスタムエンドポイント、HTTPキャッシング、永続クエリなどを扱うもの)を削除するだけで、「Private GraphQL API」を簡単に作成できます。

最後に、スコープ適用が容易なため(上記で説明したとおり)、必要なパッケージすべてにプレフィックスを付けることができるので、Private GraphQL APIは競合なしに動作します(2つの異なるプラグインが異なるバージョンのPrivate GraphQL APIをバンドルする場合に発生する可能性があります)。

このアプローチのデメリット

言うまでもなく、このアプローチは完璧にはほど遠いです。

より多くの労力が必要で、コードが冗長になる

通常、アプリケーションがWordPressで動作する場合、投稿のリストを取得するには単にget_postsを実行するだけです。シンプルで簡単です。

CMS非依存にすることで、事態は大幅に複雑になります。投稿のリストを取得するには、次のことが必要です:

  • postsパッケージとposts-wpパッケージを作成する
  • postsパッケージにgetPosts関数を持つコントラクトを作成する
  • posts-wpパッケージでget_postsを通じてコントラクトを満たす
  • 常に機能をコントラクトを通じて呼び出し、直接呼び出さないことを徹底する

(かなりの確率で)依存性注入が必要

CMS非依存パッケージのすべてのコントラクトと、CMS固有パッケージからのその実装をバインドする必要があります。私の場合、SymfonyのDependencyInjectionコンポーネントが提供するサービスコンテナを使用しています。

このアプローチが大好きで、アプリケーションを大幅に簡素化すると考えています。しかし、すべてのアプリケーションが依存性注入を必要とするわけではなく、その分複雑さが増すことは理解しています。

(最も高い確率で)モノレポが必要

Gato GraphQLは結局91パッケージを含むことになりました。かつては各パッケージを独自のリポジトリにホストしており、PRの作成が非常に難しくなっていました。そのためモノレポアプローチへの移行を「余儀なくされた」のです。

明確にしておきますが、私はモノレポが本当に好きです。しかし、誰もがそれを好むわけではなく、維持するための独自の労力も必要であることは理解しています。

参考リンク

WordPressウェブサイトを抽象化してCMS非依存にするための動機と戦略について、以前から書いてきました。Gato GraphQLのコードベースを分割するために適用したのは、まさにこの同じ戦略です:

補足:プラグインを構成する91パッケージの一覧

Gato GraphQLには次の91パッケージが含まれています。

エンジン機能:

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

API機能:

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

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

データモデル:

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

ニュースレターを購読する

Gato GraphQL のすべてのアップデートを把握しましょう。