🕸 Jak i gdzie GraphQL może ulepszyć WordPress, uzupełniając REST API
Aktualizacja 01/05/2024: Sprawdź porównanie Gato GraphQL vs WP REST API.
W ubiegły weekend opublikowałem wpis na blogu 🦸🏿♂️ Gato GraphQL jest teraz transpilowany z PHP 8.0 do 7.1.
Po podzieleniu się postem na Reddit's /r/php, społeczność rozpoczęła ożywioną dyskusję na temat tego, na ile opłaca się używać GraphQL w WordPress, jak bardzo różni się od WP REST API i czy uzasadnione jest wprowadzanie kolejnego API do WordPress.
Myślę, że większość komentarzy jest trafna, a inne pomijają pewne kluczowe informacje. GraphQL to nie tylko interfejs, ale także implementacja. Oznacza to, że różne serwery GraphQL, od różnych dostawców, mogły zostać zaprojektowane z myślą o różnych priorytetach. Dlatego nie zawsze możemy mieć jednolite oczekiwania co do tego, co GraphQL oferuje, ani pełne zrozumienie tego, jak działa silnik GraphQL.
Na przykład doświadczenie z GraphQL w WordPress i w Laravel będzie różne, podobnie jak doświadczenie oferowane przez różne serwery: WPGraphQL lub Gato GraphQL.
Ten artykuł to moje spojrzenie na sprawę, odpowiadające na kilka komentarzy z posta na Reddicie.
GraphQL vs WP REST API
[Jaki zły pomysł] mieć API GraphQL na szczycie WordPress, który już używa własnego REST API. Po prostu użyj REST API. [Source]
Zarówno REST API, jak i GraphQL służą temu samemu celowi: dostarczeniu aplikacji potrzebnych danych. Jednak różnią się sposobem osiągania tego celu: REST ma predefiniowane endpointy dostarczające określony zestaw danych, podczas gdy GraphQL może dostarczyć dokładnie te dane, których potrzebujemy.
To różne zachowanie może mieć bezpośredni wpływ na wydajność aplikacji. W przypadku REST, jeśli musimy pobrać listę postów oraz dane każdego autora, wymagane jest wysłanie dodatkowych żądań. Możliwe 1 dodatkowe żądanie dla wszystkich danych autorów lub 1 dodatkowe żądanie na każdego autora. W tym czasie odwiedzający witrynę może czekać na wyrenderowanie strony.
GraphQL poprawia tę sytuację, ponieważ możemy pobrać wszystkie dane postów i autorów w jednym żądaniu, a renderowanie strony będzie szybsze:
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}Dlatego nawet jeśli mamy już REST API w WordPress, nie oznacza to, że jest ono zawsze najbardziej odpowiednim narzędziem do każdego zadania. Oczywiście zawsze możemy go używać, ale jeśli mamy też dostęp do GraphQL, możemy zdecydować się na użycie tego API zawsze, gdy zapewnia przewagę nad REST, i wyjdziemy na tym lepiej.
Trudna konfiguracja początkowa dla GraphQL + Konieczność pisania resolverów
Zdecydowanie można argumentować, że konfiguracja początkowa dla GraphQL jest wykładniczo wyższa niż dla REST; masz rację, że skojarzenia muszą być skonfigurowane. [Source]
I...
To, co ty i prawie wszyscy inni w sieci pomijają, to fakt, że aby ten format API działał, trzeba napisać parser (resolvery + typy), co wiąże się z szeregiem problemów nieobecnych w REST. [Source]
Te komentarze nie są w pełni trafne, ponieważ zarówno WPGraphQL, jak i Gato GraphQL odwzorowały już model danych WordPress w schemacie GraphQL (WPGraphQL w całości, mój plugin w większości).
Zatem po zainstalowaniu któregokolwiek z tych pluginów można od razu zacząć pobierać dane dla swojej aplikacji, bez potrzeby tworzenia jakiegokolwiek resolvera ani konfigurowania skojarzeń między encjami.
Prawdą jest, że aby pobierać niestandardowe dane z własnych encji aplikacji (takich jak CPT), muszą one być odwzorowane przez resolvery i będziesz musiał to zrobić. Ale to nie różni się od REST: jeśli potrzebujesz niestandardowych danych ze swojego CPT, będziesz musiał utworzyć endpoint REST, aby je pobrać. Niestandardowy endpoint to też resolver.
Dlatego, jeśli chodzi o potrzebę resolverów, REST i API GraphQL są praktycznie takie same.
Teraz, przeglądając strony internetowe i dokumentację, odnosi się wrażenie, że GraphQL wymaga więcej wysiłku w konfiguracji. Jest więc ziarno prawdy w tym przekonaniu.
Myślę, że jest kilka powodów. Po pierwsze, GraphQL obejmuje (przynajmniej) dwie części:
- koncepcję tego, czym jest i jak działa
- serwery zapewniające konkretną implementację
Przeglądając dokumentację GraphQL, np. oficjalną stronę graphql.org, skupia się ona na koncepcjach stojących za GraphQL, szczegółowo omawiając resolvery, czym są i dlaczego są potrzebne.
Jest to przydatne, gdy budujesz aplikację od podstaw, np. używając Laravel i Lighthouse. W takim przypadku rzeczywiście musisz napisać swoje resolvery (ale tak samo musiałbyś tworzyć swoje endpointy REST).
Jednak WordPress jest już aplikacją, a WPGraphQL i Gato GraphQL są gotowymi rozwiązaniami. Te dwa pluginy już stworzyły dla nas resolvery, więc nie musimy się o nie martwić (podobnie jak WP REST API dostarcza wstępny zestaw endpointów, więc nie musimy się o nie troszczyć).
Ponadto GraphQL jest bardziej zorientowany na deweloperów, a jego dokumentacja zdaje się przemawiać bezpośrednio do nich. Deweloperzy tworzą resolvery po stronie serwera, a deweloperzy konsumują te resolvery za pomocą niestandardowych queries po stronie klienta. Ponieważ budowanie resolverów jest zadaniem deweloperów, pojawia się naturalnie i często.
W przypadku REST oczekiwanie (jak sądzę) jest takie, że endpoint dostarczający wymagane dane już istnieje (jak udostępniany przez WP REST API). Jeśli nie istnieje, dopiero wtedy musimy martwić się o skonfigurowanie niestandardowego endpointu. Dlatego jest mniejszy nacisk na tworzenie resolverów dla REST.
Ostatecznie zarówno REST, jak i GraphQL dostarczają wymaganych danych. Ale podczas gdy REST zachęca do statycznego podejścia, gdzie endpointy powinny już istnieć, a martwimy się o nie tylko gdy nie istnieją, GraphQL zachęca do dynamicznego podejścia, gdzie każde zapytanie jest tworzone na miarę, a my możemy napisać dla niego doskonały resolver.
Więc ostatecznie nie ma fundamentalnych różnic między REST a GraphQL, tylko różne interpretacje tego, jak mają spełniać swoje wymagania.
Podatności + Kwestie bezpieczeństwa w GraphQL
Kiedyś zobaczymy ogromną lukę w GraphQL, bo pisanie bezpiecznych interpreterów jest naprawdę trudne. [Source]
I...
WordPress jest już tak masywny, że ma ogromny cel na plecach; dodanie JAKIEGOKOLWIEK pluginu wiąże się z dużym ryzykiem, a plugin oferujący dosłowne odsłonięcie całego WordPress, w tym wielu przykładów kodu do obejścia modelu bezpieczeństwa, to dla mnie zdecydowane nie. Dane wyjściowe niekierowane przez motyw powinny być jak najbardziej ograniczone (nieistniejące, chyba że poproszę) poza tym, co absolutnie niezbędne do ujawnienia. Mam nadzieję, że to nigdy nie trafi do rdzenia. [Source]
GraphQL rzeczywiście nakłada dodatkowe ryzyko bezpieczeństwa, którym musimy się zająć. W pełni zgadzam się z tym odczuciem.
Ale nie sądzę, żeby był to aż tak blokujący problem, który uniemożliwiałby potencjalne włączenie GraphQL do rdzenia WP. Co więcej, nie uważam nawet, żeby było to naprawdę trudne do rozwiązania.
To, czego potrzeba, to żeby serwer GraphQL oparł się na istniejących mechanizmach bezpieczeństwa WordPress, a następnie żeby deweloper używał tych mechanizmów, upewniając się, że dane pole może być dostępne tylko przez odpowiednich użytkowników:
- czy użytkownik jest zalogowany?
- czy użytkownik jest administratorem?
- czy użytkownik ma jakąś rolę lub uprawnienie?
- czy użytkownik jest autorem posta?
Aby spełnić tę propozycję, Gato GraphQL oferuje Listy Kontroli Dostępu, dzięki czemu możemy zdefiniować, kto może uzyskać dostęp do każdego pola i dyrektywy, przez konfigurację.
Czasami jednak samo użycie ACL nie wystarczy i serwer GraphQL musi zapewnić dodatkowe środki bezpieczeństwa. Opiszę, nad czym teraz pracuję dla nadchodzącej wersji v0.8 Gato GraphQL.
Pole posts (do pobierania danych postów) nie wymaga autoryzacji, każdy użytkownik może do niego uzyskać dostęp, zarówno zalogowany, jak i nie. Dlatego ze względów bezpieczeństwa pobiera tylko opublikowane posty.
Ale są sytuacje, gdy musimy pobierać również posty w wersji roboczej/oczekujące/w koszu, na przykład:
- Do budowania statycznej strony, wykonywanej przez administratora, z dostępem do wszystkich danych z witryny
- Dla autorów postów, aby wylistować wszystkie szkice, które mogą dalej edytować
Opracowałem więc następujący schemat. Aby pobierać posty, będą 3 pola:
posts: otwarte dla wszystkich, może pobierać tylko opublikowane postymyPosts: otwarte dla wszystkich, pobiera tylko posty zalogowanego użytkownika, z dowolnym statusem (opublikowany/szkic/oczekujący/w koszu)postsForAdmin: tylko administrator może uzyskać do niego dostęp, pobiera dowolny post z dowolnym statusem
A następnie postsForAdmin jest domyślnie wyłączone, więc nie pojawia się nawet w schemacie GraphQL, chyba że administrator wyraźnie je włączy (i najprawdopodobniej będzie włączone tylko do budowania statycznych stron).
Inna sytuacja ma miejsce, gdy dane pole może pobierać zarówno dane publiczne, jak i prywatne. Na przykład pole option pobiera dane z tabeli wp_options. Niektóre wpisy są publiczne (np. blogname), podczas gdy inne nie są (np. admin_email).
Podobna sytuacja dotyczy pobierania wartości meta, przez pola Post.metaValue, User.metaValue i inne. Na przykład meta użytkownika zawiera wpis wp_capabilities, który jest z pewnością prywatny, podczas gdy description jest publiczny. A potem jest last_name, który może być publiczny lub prywatny w zależności od aplikacji.
Aby uczynić dostęp do tych danych bezpiecznym, plugin umożliwi określenie, które wpisy mogą być odpytywane za pomocą listy dozwolonych/odrzuconych na stronie ustawień, akceptując zarówno pełny wpis, jak i wyrażenie regularne:

Zapytanie o dozwoloną opcję będzie działać, podczas gdy odrzucona opcja po prostu zwróci null:
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}Przy odpowiednich środkach bezpieczeństwa zapewnianych przez serwer GraphQL i zdrowym rozsądku ze strony dewelopera, stworzenie bezpiecznego API GraphQL nie powinno być trudne.
GraphQL przeciążający bazę danych
GraphQL to bogata składnia pozwalająca na wyrażanie głębokich relacyjnych queries, więc dla ekosystemu takiego jak WordPress, gdzie rozszerzalność modelu danych pochodzi z wzorca entity-attribute-value, przekłada się to na niesamowite ilości obciążenia bazy danych, co może spowodować, że twoja witryna przestanie odpowiadać, jeśli zapytanie GraphQL jest głębokie, skomplikowane lub rekurencyjne. WordPress jest już słynny z tego, że potrafi powalić instancję MySQL/MariaDB, więc dodanie GraphQL mogłoby to znacznie pogorszyć, jeśli queries nie będą odpowiednio napisane, uwierzytelnione i z ograniczeniem częstotliwości. [Source]
Przeciążenie bazy danych to poważna obawa dla serwerów GraphQL. Opiszę, jak Gato GraphQL stara się unikać tego scenariusza.
Gato GraphQL zapobiega wystąpieniu problemu N+1, już przez projekt architektoniczny. Osiąga to przez powierzenie silnikowi odpowiedzialności za ładowanie encji z bazy danych, nie deweloperowi.
Przy rozwiązywaniu połączeń w resolverze zwracaną wartością jest ID (lub lista ID) obiektu(ów), a nie sam obiekt. Na przykład pobieranie autora niestandardowego posta jest wykonywane w ten sposób:
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}Mając ID encji bazy danych z resolveValue i typ obiektu z resolveFieldTypeResolverClass (reprezentowany przez klasę UserTypeResolver), silnik GraphQL może następnie załadować dane dla obiektu.
Aby załadować dane, silnik używa niezwykle wydajnego algorytmu: ma złożoność czasową O(n), gdzie n to liczba typów w zapytaniu, a nie liczba węzłów.
Algorytm osiąga tę wydajność, ponieważ nie przechodzi przez graf, lecz konwertuje strukturę danych na stos komponentów, który jest znacznie prostszy do rozwiązania. ("Graf" w GraphQL to pojęcie, a nie konkretna implementacja.)
Nawet jeśli zapytanie ma wiele poziomów, każdy pobierający wiele encji, algorytm nadal radzi sobie z tym całkiem dobrze. Na przykład nie ma dużego wpływu podczas wykonywania następującego zapytania, które ma głębokość 10 poziomów:
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}Wyjątkiem od tej wydajności jest pobieranie wartości meta przez Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue i PostCategory.metaValue (a także ich pole metaValues). Wynika to z faktu, że funkcje WordPress (get_post_meta, get_user_meta itp.) pobierają dane dla 1 ID naraz, co oznacza, że każda encja będzie wymagała wywołania bazy danych w celu pobrania wartości meta. W rezultacie rozwiązywanie wartości meta skaluje się na podstawie liczby węzłów, a nie liczby typów (komentarz OP trafnie to ujmuje).
Aby uniemożliwić złośliwym podmiotom używanie i nadużywanie pól meta, Gato GraphQL (w wersji v0.8) będzie dostarczany z tymi polami domyślnie wyłączonymi. Następnie administrator musi je wyraźnie włączyć i, robiąc to, może umieścić te pola pod jakąś Listą Kontroli Dostępu, tak aby baza danych w żadnym momencie nie była narażona na atak.
Ograniczanie częstotliwości to też świetny pomysł, planuję go obsługiwać w jakimś przyszłym wydaniu.
A potem jest analiza i narzucanie ograniczeń na złożoność zapytania (np. ile poziomów głębokości ma). Serwer GraphQL rozwiązuje zapytanie ze złożonością czasową O(n), więc nie ma zbyt wiele szkód, które mogą wynikać z pętli. Jednak pojedyncze zapytanie nadal mogłoby pobierać nieograniczone ilości danych z bazy danych i to coś, czego możemy chcieć uniknąć.
Na przykład to proste zapytanie przyniesie ogromną ilość danych w jednym żądaniu (moja strona demonstracyjna ledwie ma kilkaset rekordów, więc mogę sobie pozwolić na zademonstrowanie wykonania zapytania):
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}Jak widać, zapytanie nie musi być nawet zagnieżdżone, żeby sprawiać kłopoty. Dlatego analizowanie złożoności zapytania to delikatna kwestia, która wymaga precyzyjnego dostrojenia, aby być użyteczna.
Mam nadzieję na obsługę analizy queries, ale nie jest to na mojej liście wysokich priorytetów, ponieważ kombinacja innych funkcji (takich jak utrwalone queries lub niestandardowe endpointy w połączeniu z Listami Kontroli Dostępu) pozwala nam już trzymać złośliwe podmioty z dala, a my sami nie będziemy (nie powinniśmy!) nadużywać własnego serwisu GraphQL.