Blog

🦸🏻‍♂️ Giới thiệu: Headless WordPress không cần WordPress

Leonardo Losoviz
Bởi Leonardo Losoviz ·

Kể từ vụ tranh cãi giữa Matt Mullenweg và WPEngine, tôi nhận thấy ngày càng nhiều người trên Reddit (và các nơi khác) hỏi về các giải pháp thay thế cho WordPress — không nhất thiết là để rời bỏ WordPress (ít nhất là chưa ngay), mà để hiểu họ có những lựa chọn nào và việc di chuyển tiềm năng sẽ khó khăn đến mức nào. Họ muốn biết cách phòng ngừa rủi ro.

Đối với những ai đang làm việc với headless WordPress, Gato GraphQL nay cung cấp một tính năng mới thú vị: Headless WordPress không cần WordPress.

Bài viết này giải thích tất cả về điều đó, mô tả cách nó trở nên khả thi và trình chiếu một video minh họa.

Chạy Gato GraphQL như một ứng dụng PHP độc lập

Gato GraphQL được xây dựng bằng các thành phần PHP độc lập, được quản lý qua Composer, theo cách mà tất cả các thành phần PHP tạo nên máy chủ GraphQL không phụ thuộc vào WordPress!

Do đó, máy chủ GraphQL có thể chạy như một ứng dụng PHP độc lập, và bạn có thể tích hợp nó vào bất kỳ ứng dụng PHP nào, dù dựa trên WordPress hay bất cứ thứ gì khác.

Nếu với một số trường hợp sử dụng mà ứng dụng của bạn không cần truy cập dữ liệu WordPress, thì ít nhất với những trường hợp đó, bạn đã sẵn sàng.

Video này minh họa một trường hợp sử dụng như vậy: Tương tác với API của GitHub, để tải xuống/cài đặt các artifact từ GitHub Actions trong quá trình phát triển:

Demo Headless WordPress không cần WordPress: Thực thi GraphQL query

Trong video, GraphQL query thực hiện một yêu cầu HTTP để lấy các plugin Gato GraphQL mới nhất được tạo ra trong GitHub Actions, được tải lên dưới dạng artifact khi merge một pull request.

Các URL của các artifact từ phản hồi GraphQL sau đó được đưa vào WP-CLI, để các plugin được tự động cài đặt trên một máy chủ web DEV cục bộ, nhằm chạy các bài kiểm thử.

(Tôi sẽ giải thích chi tiết hơn ở phần cuối của bài viết này.)

Trong trường hợp sử dụng này, vì không có dữ liệu WordPress nào được truy cập, máy chủ GraphQL đã có thể chạy như một ứng dụng PHP độc lập.

Nếu cần, tôi thậm chí có thể sử dụng nó trong workflow GitHub Actions của mình!

Di chuyển một ứng dụng headless WordPress

Mỗi khi bạn thực sự truy cập dữ liệu WordPress, hãy xem cách chạy điều đó mà không cần WordPress.

Schema GraphQL do Gato GraphQL cung cấp chứa các trường để lấy dữ liệu WordPress: posts, users, comments, tags, categories, v.v.

Mã trong các resolver PHP để lấy dữ liệu WordPress phụ thuộc vào WordPress; mã đó không thể chạy trên ứng dụng không phải WordPress.

Tuy nhiên, Gato GraphQL có mỗi resolver được triển khai thông qua 2 package:

  1. Một package PHP "vanilla", chứa tất cả mã chung
  2. Một package dành riêng cho WordPress, chứa các lời gọi thực tế đến các phương thức WordPress để thỏa mãn resolver đó

Ví dụ, trong GraphQL query này:

{
  posts {
    id
    title
  }
}

...logic để lấy các bài viết bao gồm:

  1. Trường Root.posts: Nằm trong package posts chung
  2. Cách giải quyết của nó cho WordPress thông qua phương thức get_posts: Nằm trong package posts-wp dành riêng cho WordPress.

Sự phân chia mã giữa các package không phải WordPress/WordPress là khoảng 80/20%, nghĩa là 80% mã có thể tái sử dụng với framework/CMS khác, và chỉ 20% mã cần được triển khai lại.

Hơn nữa, tất cả các chức năng trong Gato GraphQL được cung cấp qua các module, và các module có thể được bật/tắt tùy ý.

Các module schema
Các module schema

Modules là một tính năng được triển khai vì mục đích bảo mật: Nếu bạn không cần hiển thị dữ liệu người dùng trong API công khai của mình, bạn có thể tắt module Users, và các trường tương ứng (như Root.users) sẽ không bao giờ được thêm vào schema.

Các module được ánh xạ trực tiếp đến các package PHP bên dưới. Do đó, khi chạy Gato GraphQL như một ứng dụng độc lập, chúng ta có thể chọn lọc tải những module/package mà chúng ta cần, và không tải những module/package còn lại.

Ví dụ, nếu ứng dụng của bạn chỉ in dữ liệu cho các bài viết, danh mục và thẻ, thì chỉ cần tải các package posts-wp, categories-wptags-wp (cùng với các dependency của chúng).

Sau đó, khi di chuyển khỏi WordPress (chẳng hạn sang Laravel hoặc Symfony), chỉ có 3 package dành riêng cho WordPress đó cần được triển khai lại cho framework/CMS mới, và không có gì khác.

Do đó, bạn có thể sử dụng headless WordPress ngay hôm nay, biết rằng sau này bạn có thể di chuyển ứng dụng của mình sang framework hoặc CMS khác với nỗ lực tối thiểu.

Chuyển đổi sang Gato GraphQL từ một API khác

Nếu bạn đã sử dụng headless WordPress, rất có thể ứng dụng của bạn đang dùng WP REST API hoặc WPGraphQL.

Thật không may, với bất kỳ API nào trong số hai API này, bạn bị ràng buộc với WordPress: Không có WP REST API nào bên ngoài WordPress, và WPGraphQL không thể chạy mà không có WordPress.

May mắn thay, có thể thay thế một trong số chúng bằng Gato GraphQL, và giành được khả năng di chuyển ứng dụng headless WordPress của bạn ra khỏi WordPress.

Khi đó sẽ cần thực hiện 2 bước sau:

  1. Chuyển từ WP REST API hoặc WPGraphQL sang Gato GraphQL
  2. Triển khai lại các package dành riêng cho WordPress cần thiết

Hãy xem cách thực hiện việc chuyển đổi API.

WP REST API sang persisted queries của Gato GraphQL

Với extension Persisted Queries bạn có thể xuất bản các endpoint kiểu REST, được cấu thành bằng GraphQL.

Với mỗi endpoint REST trong ứng dụng của bạn, bạn có thể tạo một endpoint persisted query tương ứng để lấy cùng dữ liệu đó, và sử dụng endpoint đó thay thế.

Ví dụ, GraphQL query sau có thể thay thế endpoint 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
  }
}

Nhờ vào hệ thống phân cấp API, persisted query có thể được xuất bản dưới đường dẫn /graphql-query/wp/v2/posts/, giúp dễ dàng ánh xạ các endpoint.

Để sao chép endpoint REST /wp-json/wp/v2/posts/{id}/, vốn lấy dữ liệu cho bài viết có ID đã cho, chúng ta có thể cung cấp ID bài viết qua tham số URL postId.

Ví dụ, persisted query sau có thể được gọi qua endpoint /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 sang Gato GraphQL

Schema GraphQL từ WPGraphQL và Gato GraphQL tương tự nhau nhưng hơi khác biệt, vì vậy chúng cần được điều chỉnh.

Starter WordPress Next.js leoloso/next-wordpress-starter hoạt động với cả WPGraphQL lẫn Gato GraphQL. Starter sử dụng cùng một logic JS cho cả hai máy chủ, chỉ có các GraphQL queries là khác nhau.

Starter này cung cấp nhiều ví dụ về việc điều chỉnh các queries giữa hai máy chủ. Ví dụ, WPGraphQL query này:

fragment PostFields on Post {
  id
  categories {
    edges {
      node {
        databaseId
        id
        name
        slug
      }
    }
  }
  databaseId
  date
  isSticky
  postId
  slug
  title
}

...được điều chỉnh như thế này cho 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
}

Chi tiết: Chạy Gato GraphQL như một ứng dụng PHP độc lập

Đây là giải thích chi tiết về video minh họa từ phần trước.

Chúng ta cung cấp GraphQL query để chạy trong file retrieve-github-artifacts.gql.

Query kết nối đến API GitHub bằng cách lấy access token từ biến môi trường GITHUB_ACCESS_TOKEN. Nó tự động tạo đường dẫn đầy đủ cho endpoint actions/artifacts từ các biến được cung cấp, rồi gửi một yêu cầu HTTP đến đó.

Từ phản hồi, nó trích xuất "download URL" từ bên trong mỗi mục artifact, và gửi các yêu cầu HTTP bất đồng bộ đến chúng. Từ header Location của mỗi "download URL" này, chúng ta lấy được URL thực của file có thể tải xuống.

Cuối cùng, nó in tất cả các URL cùng nhau, phân cách bằng dấu cách, để thuận tiện cho việc đưa vào WP-CLI.

# 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: " "
  )
}

Logic PHP trực tiếp tải mã từ plugin Gato GraphQL, và từ bundle "Power Extensions" (cần thiết để gửi các yêu cầu HTTP và các chức năng khác).

Là một ứng dụng PHP độc lập, chúng ta phải chỉ định rõ ràng những module nào được khởi tạo, và cung cấp bất kỳ cấu hình nào không mặc định.

Ví dụ, chúng ta báo cho module SendHTTPRequests cho phép kết nối đến https://api.github.com/repos, và module EnvironmentFields cho phép truy cập biến môi trường GITHUB_ACCESS_TOKEN.

Lưu ý rằng schema GraphQL được tạo ra lần đầu tiên khi GraphQL query được thực thi, và được lưu vào cache trên đĩa. Theo cách này, từ lần thứ 2 trở đi, không có mã nào để tính toán schema được thực thi, giúp quá trình thực thi nhanh hơn.

Cuối cùng, ứng dụng độc lập khởi tạo máy chủ GraphQL, thực thi query đối với nó, và in ra phản hồi.

<?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();

Để thực thi GraphQL query, chúng ta chạy trong terminal (dùng jq để in đẹp JSON output):

php retrieve-github-artifacts.php | jq

Cuối cùng, để trích xuất các URL artifact từ phản hồi GraphQL, và đưa chúng vào WP-CLI, chúng ta chạy:

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

Như được thể hiện trong video, chúng ta có thể thực thi Gato GraphQL mà không cần WordPress.


Đăng ký nhận bản tin của chúng tôi

Cập nhật tất cả những điều mới từ Gato GraphQL.