🦸🏻♂️ 紹介:WordPressなしのHeadless WordPress
Matt Mullenweg対WPEngineの騒動以来、Reddit(やその他の場所)でWordPressの代替を求める人々がますます増えていることに気づきました。必ずしもWordPressから離れたいわけではなく(少なくともすぐには)、どんな選択肢があるのか、また潜在的な移行がどれほど大変かを理解したいのです。リスクを分散させる方法を知りたいのです。
Headless WordPressで作業している方々に向けて、Gato GraphQLはクールな新機能を提供します:WordPressなしのHeadless WordPress。
この記事では、それがどのように実現可能なのかを説明し、デモ動画をご覧いただきます。
Gato GraphQLをスタンドアロンPHPアプリとして実行する
Gato GraphQLは、Composerで管理されたスタンドアロンPHPコンポーネントを使って構築されており、GraphQLサーバーを構成するすべてのPHPコンポーネントがWordPressに依存していません!
そのため、GraphQLサーバーはスタンドアロンPHPアプリケーションとして実行でき、WordPressベースの、あるいは他のどんなPHPアプリケーションにも組み込むことができます。
ユースケースによってはWordPressのデータにアクセスする必要がない場合、少なくともそのユースケースについてはすぐに利用を開始できます。
この動画では、そのようなユースケースを実演します:GitHub APIと連携し、開発中にGitHub Actionsからアーティファクトをダウンロード・インストールする様子です:
動画では、GraphQLクエリがHTTPリクエストを実行し、GitHub Actionsで生成された最新のGato GraphQLプラグインを取得しています。これらはプルリクエストのマージ時にアーティファクトとしてアップロードされます。
GraphQLレスポンスから得たアーティファクトのURLはWP-CLIに注入され、プラグインがローカルのDEVウェブサーバーに自動インストールされ、テストを実行できる状態になります。
(詳細はこの記事の最後のセクションで説明します。)
このユースケースでは、WordPressのデータにまったくアクセスしないため、GraphQLサーバーはすでにスタンドアロンPHPアプリとして実行できます。
必要であれば、GitHub Actionsのワークフロー内でも使用できます!
Headless WordPressアプリを移行する
WordPressのデータにアクセスする場合でも、WordPressなしでそれを実行する方法を見てみましょう。
Gato GraphQLが提供するGraphQLスキーマには、WordPressデータを取得するフィールドが含まれています:投稿、ユーザー、コメント、タグ、カテゴリーなどです。
WordPressデータを取得するPHPリゾルバーのコードはWordPressに依存しており、そのコードはWordPress以外のアプリでは実行できません。
しかし、Gato GraphQLではこれらのリゾルバーはそれぞれ2つのパッケージで実装されています:
- 「バニラ」PHP版:汎用コードをすべて含むもの
- WordPress固有版:そのリゾルバーを満たすWordPressメソッドへの実際の呼び出しを含むもの
たとえば、このGraphQLクエリでは:
{
posts {
id
title
}
}...投稿を取得するロジックは以下で構成されています:
Root.postsフィールド:汎用のpostsパッケージに存在しますget_postsメソッドによるWordPress向けの解決:WordPress固有のposts-wpパッケージに存在します。
WordPress以外のパッケージとWordPressパッケージのコード分割は約80/20%となっており、つまりコードの80%は別のフレームワーク/CMSでも再利用可能で、20%のコードだけを再実装すればよいということです。
さらに、Gato GraphQLのすべての機能はモジュールを通じて提供され、モジュールは自由に有効化・無効化できます。

Modulesはセキュリティ目的で実装された機能です:パブリックAPIでユーザーデータを公開する必要がない場合、Usersモジュールを無効化すれば、対応するフィールド(Root.usersなど)がスキーマに追加されることはありません。
モジュールは基盤となるPHPパッケージに直接マッピングされています。 そのため、Gato GraphQLをスタンドアロンアプリとして実行する際、必要なモジュール・パッケージだけを選択的に読み込み、他は読み込まないようにできます。
たとえば、アプリケーションが投稿、カテゴリー、タグのデータのみを表示するなら、posts-wp、categories-wp、tags-wpパッケージ(およびその依存関係)だけを読み込めばよいのです。
そして、WordPressから別のフレームワーク(LaravelやSymfonyなど)に移行する際も、その3つのWordPress固有パッケージだけを新しいフレームワーク/CMS向けに再実装すればよく、それ以外は何も変更する必要がありません。
結果として、今日からHeadless WordPressを使い始め、将来的には最小限の労力でアプリケーションを別のフレームワークやCMSに移行できるという安心感を持てます。
別のAPIからGato GraphQLへの移行
すでにHeadless WordPressを行っている場合、アプリはWP REST APIかWPGraphQLのいずれかを使用している可能性が高いです。
残念ながら、これらの2つのAPIのいずれかを使用している場合、WordPressに縛られることになります:WordPress以外にWP REST APIは存在せず、WPGraphQLはWordPressなしでは動作しません。
幸いなことに、それらのいずれかをGato GraphQLに置き換えることができ、Headless WordPressアプリをWordPressから移行する能力を得ることができます。
その場合、以下の2つのステップが必要になります:
- WP REST APIまたはWPGraphQLからGato GraphQLへの移行
- 必要なWordPress固有パッケージの再実装
APIの移行をどのように行うかを見ていきましょう。
WP REST APIからGato GraphQLのpersisted queriesへ
Persisted Queries拡張機能を使用すると、GraphQLで構成されたRESTライクなエンドポイントを公開できます。
アプリケーションの各RESTエンドポイントに対して、同じデータを取得する対応するpersisted queryエンドポイントを作成し、代わりにそのエンドポイントを使用できます。
たとえば、以下のGraphQLクエリはRESTエンドポイント/wp-json/wp/v2/posts/を置き換えられます:
{
posts {
id
date: dateStr(format: "Y-m-d\\TH:i:s")
modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
slug
status
link: url
title: self {
rendered: title
}
content: self {
rendered: content
},
excerpt: self {
rendered: excerpt
}
author
featured_media: featuredImage
sticky: isSticky
categories
tags
}
}APIヒエラルキーを活用することで、persisted queryを/graphql-query/wp/v2/posts/というパスで公開でき、エンドポイントのマッピングが容易になります。
指定されたIDの投稿データを取得するRESTエンドポイント/wp-json/wp/v2/posts/{id}/を再現するには、URLパラメーターpostIdに投稿IDを指定できます。
たとえば、以下のpersisted queryはエンドポイント/graphql-query/wp/v2/posts/single/?postId={id}で呼び出せます:
query GetPost($postId: ID!) {
post(by: { id: $postId }) {
id
date: dateStr(format: "Y-m-d\\TH:i:s")
modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
slug
status
link: url
title: self {
rendered: title
}
content: self {
rendered: content
},
excerpt: self {
rendered: excerpt
}
author
featured_media: featuredImage
sticky: isSticky
categories
tags
}
}WPGraphQLからGato GraphQLへ
WPGraphQLとGato GraphQLのGraphQLスキーマは類似していますが若干異なるため、適応が必要です。
Next.js WordPressスターターleoloso/next-wordpress-starterはWPGraphQLとGato GraphQLの両方で動作します。スターターは両方のサーバーに同じJSロジックを使用し、異なるのはGraphQLクエリだけです。
このスターターでは、2つのサーバー間でクエリを適応させる例がいくつか提供されています。たとえば、このWPGraphQLクエリは:
fragment PostFields on Post {
id
categories {
edges {
node {
databaseId
id
name
slug
}
}
}
databaseId
date
isSticky
postId
slug
title
}...Gato GraphQL向けにこのように適応されます:
fragment PostFields on Post {
id
categories: self {
edges: categories(pagination: { limit: -1 }) {
node: self {
databaseId: id
id
name
slug
}
}
}
databaseId: id
date: dateStr
isSticky
postId: id
slug
title
}詳細:Gato GraphQLをスタンドアロンPHPアプリとして実行する
ここでは、先ほどのデモ動画の詳細な説明を行います。
実行するGraphQLクエリはファイルretrieve-github-artifacts.gqlに記述します。
クエリは環境変数GITHUB_ACCESS_TOKENからアクセストークンを取得してGitHub APIに接続します。提供された変数からactions/artifactsエンドポイントのフルパスを動的に生成し、HTTPリクエストを送信します。
レスポンスから各アーティファクトアイテム内の「ダウンロードURL」を抽出し、それらに対して非同期HTTPリクエストを送信します。各「ダウンロードURL」のLocationヘッダーから、実際のダウンロード可能ファイルのURLを取得します。
最後に、WP-CLIへの注入に便利なように、すべてのURLをスペース区切りで出力します。
# File retrieve-github-artifacts.gql
query RetrieveProxyArtifactDownloadURLs(
$repoOwner: String!
$repoProject: String!
$perPage: Int = 1
$artifactName: String = ""
) {
githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
@remove
# Create the authorization header to send to GitHub
authorizationHeader: _sprintf(
string: "Bearer %s"
values: [$__githubAccessToken]
)
@remove
# Create the authorization header to send to GitHub
githubRequestHeaders: _echo(
value: [
{ name: "Accept", value: "application/vnd.github+json" }
{ name: "Authorization", value: $__authorizationHeader }
]
)
@remove
@export(as: "githubRequestHeaders")
githubAPIEndpoint: _sprintf(
string: "https://api.github.com/repos/%s/%s/actions/artifacts?per_page=%s&name=%s"
values: [$repoOwner, $repoProject, $perPage, $artifactName]
)
# Use the field from "Send HTTP Request Fields" to connect to GitHub
gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
input: {
url: $__githubAPIEndpoint
options: { headers: $__githubRequestHeaders }
}
)
@remove
# Finally just extract the URL from within each "artifacts" item
gitHubProxyArtifactDownloadURLs: _objectProperty(
object: $__gitHubArtifactData
by: { key: "artifacts" }
)
@underEachArrayItem(passValueOnwardsAs: "artifactItem")
@applyField(
name: "_objectProperty"
arguments: { object: $artifactItem, by: { key: "archive_download_url" } }
setResultInResponse: true
)
@export(as: "gitHubProxyArtifactDownloadURLs")
}
query CreateHTTPRequestInputs
@depends(on: "RetrieveProxyArtifactDownloadURLs")
{
httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
@underEachArrayItem(passValueOnwardsAs: "url")
@applyField(
name: "_objectAddEntry"
arguments: {
object: {
options: { headers: $githubRequestHeaders, allowRedirects: null }
}
key: "url"
value: $url
}
setResultInResponse: true
)
@export(as: "httpRequestInputs")
@remove
}
query RetrieveActualArtifactDownloadURLs
@depends(on: "CreateHTTPRequestInputs")
{
_sendHTTPRequests(inputs: $httpRequestInputs) {
artifactDownloadURL: header(name: "Location")
@export(as: "artifactDownloadURLs", type: LIST)
}
}
query PrintSpaceSeparatedArtifactDownloadURLs
@depends(on: "RetrieveActualArtifactDownloadURLs")
{
spaceSeparatedArtifactDownloadURLs: _arrayJoin(
array: $artifactDownloadURLs
separator: " "
)
}PHPのロジックは、Gato GraphQLプラグインと「Power Extensions」バンドル(HTTPリクエストの送信などの機能に必要)から直接コードを読み込みます。
スタンドアロンPHPアプリとして、どのモジュールを初期化するかを明示的に指定し、デフォルト以外の設定を提供する必要があります。
たとえば、モジュールSendHTTPRequestsにhttps://api.github.com/reposへの接続を許可し、モジュールEnvironmentFieldsに環境変数GITHUB_ACCESS_TOKENへのアクセスを許可するように設定します。
GraphQLスキーマはGraphQLクエリが最初に実行されたときに生成され、ディスクにキャッシュされます。これにより、2回目以降はスキーマを計算するコードがまったく実行されないため、実行が高速になります。
最後に、スタンドアロンアプリはGraphQLサーバーを初期化し、クエリを実行して、レスポンスを出力します。
<?php
// File retrieve-github-artifacts.php
declare(strict_types=1);
use GraphQLByPoP\GraphQLServer\Server\StandaloneGraphQLServer;
use PoP\Root\Container\ContainerCacheConfiguration;
// Load the GraphQL server via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql/vendor/scoper-autoload.php');
// Load the PRO extensions via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql-power-extensions-bundle/vendor/scoper-autoload.php');
// Modules required in the GraphQL query
$moduleClasses = [
\PoPSchema\EnvironmentFields\Module::class,
\PoPSchema\FunctionFields\Module::class,
\GraphQLByPoP\ExportDirective\Module::class,
\GraphQLByPoP\DependsOnOperationsDirective\Module::class,
\GraphQLByPoP\RemoveDirective\Module::class,
\PoPSchema\ApplyFieldDirective\Module::class,
\PoPSchema\SendHTTPRequests\Module::class,
\PoPSchema\ConditionalMetaDirectives\Module::class,
\PoPSchema\DataIterationMetaDirectives\Module::class,
];
// Configure the modules
$moduleClassConfiguration = [
\PoP\GraphQLParser\Module::class => [
\PoP\GraphQLParser\Environment::ENABLE_MULTIPLE_QUERY_EXECUTION => true,
\PoP\GraphQLParser\Environment::USE_LAST_OPERATION_IN_DOCUMENT_FOR_MULTIPLE_QUERY_EXECUTION_WHEN_OPERATION_NAME_NOT_PROVIDED => true,
\PoP\GraphQLParser\Environment::ENABLE_RESOLVED_FIELD_VARIABLE_REFERENCES => true,
\PoP\GraphQLParser\Environment::ENABLE_COMPOSABLE_DIRECTIVES => true,
],
\PoPSchema\SendHTTPRequests\Module::class => [
\PoPSchema\SendHTTPRequests\Environment::SEND_HTTP_REQUEST_URL_ENTRIES => [
'#https://api.github.com/repos/(.*)#',
],
],
\PoPSchema\EnvironmentFields\Module::class => [
\PoPSchema\EnvironmentFields\Environment::ENVIRONMENT_VARIABLE_OR_PHP_CONSTANT_ENTRIES => [
'GITHUB_ACCESS_TOKEN',
],
],
];
// Cache the schema to disk, to speed-up execution from the 2nd time onwards
$containerCacheConfiguration = new ContainerCacheConfiguration('MyGraphQLServer', true, 'retrieve-github-artifacts', __DIR__ . '/tmp');
// Initialize the server
$graphQLServer = new StandaloneGraphQLServer($moduleClasses, $moduleClassConfiguration, [], [], $containerCacheConfiguration);
/**
* GraphQL query to execute, stored in its own .gql file
*
* @var string
*/
$query = file_get_contents(__DIR__ . '/retrieve-github-artifacts.gql');
// GraphQL variables
$variables = [
'repoOwner' => 'GatoGraphQL',
'repoProject' => 'GatoGraphQL',
'perPage' => 3
];
// Execute the query
$response = $graphQLServer->execute(
$query,
$variables,
);
// Print the response
echo $response->getContent();GraphQLクエリを実行するには、ターミナルで以下を実行します(jqを使ってJSONの出力を見やすく整形します):
php retrieve-github-artifacts.php | jq最後に、GraphQLレスポンスからアーティファクトのURLを抽出し、WP-CLIに注入するには、以下を実行します:
GITHUB_ARTIFACT_URLS=$(php retrieve-github-artifacts.php \
| grep -E -o '"spaceSeparatedArtifactDownloadURLs\":"(.*)"' \
| cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev \
| sed 's/\\\//\//g')
wp plugin install ${GITHUB_ARTIFACT_URLS} --force --activate動画で示したように、WordPressなしでGato GraphQLを実行できることが確認できました。