Blog

🦸🏻‍♂️ Przedstawiamy: Headless WordPress bez WordPress

Leonardo Losoviz
Autor: Leonardo Losoviz ·

Od czasu sporu między Mattem Mullenwegiem a WPEngine zauważam coraz więcej osób na Reddit (i w innych miejscach) pytających o alternatywy dla WordPress — niekoniecznie po to, by od razu z niego zrezygnować (przynajmniej nie natychmiast), ale by zrozumieć, jakie mają opcje i jak bolesna mogłaby być ewentualna migracja. Chcą wiedzieć, jak zabezpieczyć się na różne scenariusze.

Dla osób pracujących z headless WordPress, Gato GraphQL oferuje teraz ciekawą nową funkcjonalność: Headless WordPress bez WordPress.

Ten artykuł wyjaśnia wszystko na ten temat, opisując, jak jest to w ogóle możliwe, oraz prezentując wideo demonstracyjne.

Uruchamianie Gato GraphQL jako samodzielna aplikacja PHP

Gato GraphQL został zbudowany przy użyciu samodzielnych komponentów PHP, zarządzanych przez Composer, w taki sposób, że wszystkie komponenty PHP składające się na serwer GraphQL nie zależą od WordPress!

Dzięki temu serwer GraphQL może działać jako samodzielna aplikacja PHP i można go dołączyć do dowolnej aplikacji PHP, opartej na WordPress lub na czymkolwiek innym.

Jeśli w jakimś przypadku użycia twoja aplikacja nie potrzebuje dostępu do danych WordPress, to przynajmniej w tym przypadku jesteś już gotowy do działania.

To wideo demonstruje taki przypadek użycia: interakcję z API GitHub w celu pobierania/instalowania artefaktów z GitHub Actions podczas tworzenia:

Demo Headless WordPress bez WordPress: wykonywanie query GraphQL

W filmie query GraphQL wykonuje żądanie HTTP w celu pobrania najnowszych wtyczek Gato GraphQL wygenerowanych w GitHub Actions, które są przesyłane jako artefakty przy scalaniu pull requesta.

Adresy URL artefaktów z odpowiedzi GraphQL są następnie przekazywane do WP-CLI, aby wtyczki były automatycznie instalowane na lokalnym serwerze DEV w celu uruchomienia testów.

(Szczegółowo wyjaśnię to w ostatniej sekcji tego artykułu.)

W tym przypadku użycia, ponieważ nie są w ogóle dostępowane żadne dane WordPress, serwer GraphQL może już działać jako samodzielna aplikacja PHP.

Gdybym potrzebował, mógłbym nawet użyć go wewnątrz mojego workflow GitHub Actions!

Migracja aplikacji headless WordPress

Kiedy faktycznie korzystasz z danych WordPress, zobaczmy jak uruchomić to bez WordPress.

Schema GraphQL dostarczany przez Gato GraphQL zawiera pola do pobierania danych WordPress: posts, users, comments, tags, categories itp.

Kod w resolverach PHP pobierający dane WordPress zależy od WordPress; ten kod nie może działać w aplikacji innej niż WordPress.

Jednak Gato GraphQL ma każdy z tych resolverów zaimplementowany za pomocą 2 pakietów:

  1. Pakietu "vanilla" PHP, zawierającego cały ogólny kod
  2. Pakietu specyficznego dla WordPress, zawierającego rzeczywiste wywołania metod WordPress, które obsługują dany resolver

Na przykład w tej query GraphQL:

{
  posts {
    id
    title
  }
}

...logika pobierania postów składa się z:

  1. Pola Root.posts: znajduje się w ogólnym pakiecie posts
  2. Jego rozwiązania dla WordPress za pomocą metody get_posts: znajduje się w specyficznym dla WordPress pakiecie posts-wp.

Podział kodu między pakietami nie-WordPress/WordPress wynosi około 80/20%, co oznacza, że 80% kodu jest wielokrotnego użytku z innym frameworkiem/CMS, a jedynie 20% kodu wymagałoby ponownej implementacji.

Ponadto wszystkie funkcjonalności Gato GraphQL są dostarczane poprzez moduły, a moduły można włączać/wyłączać według potrzeb.

Moduły schematu
Moduły schematu

Modules to funkcjonalność zaimplementowana ze względów bezpieczeństwa: jeśli nie musisz udostępniać danych użytkowników w swoim publicznym API, możesz wyłączyć moduł Users, a odpowiednie pola (takie jak Root.users) nigdy nie zostaną dodane do schematu.

Moduły są bezpośrednio mapowane na odpowiadające im pakiety PHP. Dlatego uruchamiając Gato GraphQL jako samodzielną aplikację, możemy selektywnie ładować tylko te moduły/pakiety, których potrzebujemy, i żadne inne.

Na przykład, jeśli twoja aplikacja wyświetla tylko dane postów, kategorii i tagów, to tylko pakiety posts-wp, categories-wp i tags-wp (wraz z ich zależnościami) muszą być załadowane.

Następnie, migrując z WordPress (powiedzmy do Laravel lub Symfony), tylko te 3 pakiety specyficzne dla WordPress musiałyby zostać ponownie zaimplementowane dla nowego frameworka/CMS i nic więcej.

W konsekwencji możesz używać headless WordPress już dziś, wiedząc, że w przyszłości będziesz mógł migrować swoją aplikację do innego frameworka lub CMS przy minimalnym wysiłku.

Przejście do Gato GraphQL z innego API

Jeśli już korzystasz z headless WordPress, istnieje duże prawdopodobieństwo, że twoja aplikacja używa WP REST API lub WPGraphQL.

Niestety, z którymkolwiek z tych dwóch API jesteś uzależniony od WordPress: nie ma WP REST API poza WordPress, a WPGraphQL nie może działać bez WordPress.

Na szczęście możliwe jest zastąpienie któregoś z nich przez Gato GraphQL i uzyskanie możliwości migracji aplikacji headless WordPress z WordPress.

Potrzebne byłyby te 2 kroki:

  1. Przejście z WP REST API lub WPGraphQL do Gato GraphQL
  2. Ponowna implementacja wymaganych pakietów specyficznych dla WordPress

Zobaczmy, jak można przeprowadzić przejście API.

WP REST API do queries persistowanych Gato GraphQL

Dzięki rozszerzeniu Persisted Queries możesz publikować endpointy podobne do REST, złożone przy użyciu GraphQL.

Dla każdego z endpointów REST w twojej aplikacji możesz utworzyć odpowiadający mu endpoint query persistowanej, który pobiera te same dane, i używać go zamiast tamtego.

Na przykład następująca query GraphQL może zastąpić 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
  }
}

Dzięki hierarchii API, query persistowana może być opublikowana pod ścieżką /graphql-query/wp/v2/posts/, co ułatwia mapowanie endpointów.

Aby odtworzyć endpoint REST /wp-json/wp/v2/posts/{id}/, który pobiera dane posta o danym ID, możemy podać ID posta poprzez parametr URL postId.

Na przykład następująca query persistowana może być wywoływana pod endpointem /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 do Gato GraphQL

Schema GraphQL z WPGraphQL i z Gato GraphQL są podobne, ale nieco różne, więc wymagają adaptacji.

Starter WordPress z Next.js leoloso/next-wordpress-starter działa zarówno z WPGraphQL, jak i z Gato GraphQL. Starter używa tej samej logiki JS dla obu serwerów; różnią się jedynie queries GraphQL.

Ten starter dostarcza kilka przykładów adaptacji queries między tymi dwoma serwerami. Na przykład ta query WPGraphQL:

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

...jest adaptowana w ten sposób dla 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
}

Szczegółowo: Uruchamianie Gato GraphQL jako samodzielna aplikacja PHP

Oto szczegółowe wyjaśnienie wideo demonstracyjnego z wcześniejszej części.

Dostarczamy query GraphQL do wykonania w pliku retrieve-github-artifacts.gql.

Query łączy się z API GitHub, pobierając token dostępu ze zmiennej środowiskowej GITHUB_ACCESS_TOKEN. Dynamicznie generuje pełną ścieżkę dla endpointu actions/artifacts na podstawie podanych zmiennych, a następnie wysyła żądanie HTTP.

Z odpowiedzi wyodrębnia "URL pobierania" z każdego elementu artefaktu i wysyła asynchroniczne żądania HTTP. Z nagłówka Location każdego z tych "URL pobierania" uzyskujemy rzeczywisty URL pobieranego pliku.

Na koniec drukuje wszystkie URL razem oddzielone spacją, aby ułatwić ich wstrzyknięcie do 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: " "
  )
}

Logika PHP bezpośrednio ładuje kod z wtyczki Gato GraphQL i z pakietu "Power Extensions" (potrzebnego do wysyłania żądań HTTP i innych funkcjonalności).

Jako samodzielna aplikacja PHP musimy jawnie wskazać, które moduły są inicjalizowane, i podać wszelką niestandardową konfigurację.

Na przykład, polecamy modułowi SendHTTPRequests zezwolić na łączenie się z https://api.github.com/repos, a modułowi EnvironmentFields zezwolić na dostęp do zmiennej środowiskowej GITHUB_ACCESS_TOKEN.

Należy zauważyć, że schema GraphQL jest generowany przy pierwszym wykonaniu query GraphQL i zapisywany w pamięci podręcznej na dysku. W ten sposób od drugiego uruchomienia żaden kod obliczający schemat nie jest wykonywany, co przyspiesza działanie.

Na koniec samodzielna aplikacja inicjalizuje serwer GraphQL, wykonuje query i drukuje odpowiedź.

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

Aby wykonać query GraphQL, uruchamiamy w terminalu (używając jq do czytelnego wydruku danych wyjściowych JSON):

php retrieve-github-artifacts.php | jq

Na koniec, aby wyodrębnić URL artefaktów z odpowiedzi GraphQL i wstrzyknąć je do WP-CLI, uruchamiamy:

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

Jak pokazano w filmie, jesteśmy w stanie wykonywać Gato GraphQL bez WordPress.


Zapisz się do naszego newslettera

Bądź na bieżąco ze wszystkimi aktualizacjami Gato GraphQL.