ブログ

🦸🏻‍♂️ 紹介:WordPressなしのHeadless WordPress

Leonardo Losoviz
著者: Leonardo Losoviz ·

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からアーティファクトをダウンロード・インストールする様子です:

WordPressなしのHeadless WordPressデモ:GraphQLクエリの実行

動画では、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つのパッケージで実装されています:

  1. 「バニラ」PHP版:汎用コードをすべて含むもの
  2. WordPress固有版:そのリゾルバーを満たすWordPressメソッドへの実際の呼び出しを含むもの

たとえば、このGraphQLクエリでは:

{
  posts {
    id
    title
  }
}

...投稿を取得するロジックは以下で構成されています:

  1. Root.postsフィールド:汎用のpostsパッケージに存在します
  2. get_postsメソッドによるWordPress向けの解決:WordPress固有のposts-wpパッケージに存在します。

WordPress以外のパッケージとWordPressパッケージのコード分割は約80/20%となっており、つまりコードの80%は別のフレームワーク/CMSでも再利用可能で、20%のコードだけを再実装すればよいということです。

さらに、Gato GraphQLのすべての機能はモジュールを通じて提供され、モジュールは自由に有効化・無効化できます。

スキーマモジュール
スキーマモジュール

Modulesはセキュリティ目的で実装された機能です:パブリックAPIでユーザーデータを公開する必要がない場合、Usersモジュールを無効化すれば、対応するフィールド(Root.usersなど)がスキーマに追加されることはありません。

モジュールは基盤となるPHPパッケージに直接マッピングされています。 そのため、Gato GraphQLをスタンドアロンアプリとして実行する際、必要なモジュール・パッケージだけを選択的に読み込み、他は読み込まないようにできます。

たとえば、アプリケーションが投稿、カテゴリー、タグのデータのみを表示するなら、posts-wpcategories-wptags-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つのステップが必要になります:

  1. WP REST APIまたはWPGraphQLからGato GraphQLへの移行
  2. 必要な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アプリとして、どのモジュールを初期化するかを明示的に指定し、デフォルト以外の設定を提供する必要があります。

たとえば、モジュールSendHTTPRequestshttps://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を実行できることが確認できました。


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

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