Koncepcje, Idee, Strategie
Koncepcje, Idee, StrategieJak plugin mapuje model danych WordPress na schemat GraphQL

Jak plugin mapuje model danych WordPress na schemat GraphQL

Zobacz, jak Gato GraphQL zmapował model danych WordPress na odpowiadający mu schemat GraphQL.

Model danych WordPress

WordPress posiada następujące encje:

  • posts
  • pages
  • custom posts
  • elementy mediów
  • użytkownicy
  • role użytkowników
  • tagi
  • kategorie
  • komentarze
  • bloki
  • właściwości meta
  • inne (opcje, wtyczki, motywy itp.)

Te encje mogą mieć hierarchię. Na przykład post, page i elementy mediów to wszystko custom post types, a tagi i kategorie są obie taksonomiami.

Oto diagram bazy danych WordPress, pokazujący, jak przechowywane są dane wszystkich encji:

Diagram bazy danych WordPress

Czy mapowanie jest dokładną repliką diagramu bazy danych?

Czy podczas mapowania bazy danych WordPress na schemat GraphQL powyższy diagram jest respektowany 1 do 1?

Nie, nie jest. Podczas gdy diagram bazy danych jest rzeczywistą implementacją, GraphQL jest interfejsem do uzyskiwania dostępu do danych po stronie klienta. Te dwa elementy są ze sobą powiązane, ale mogą się różnić. GraphQL nie przejmuje się bazą danych: nie myśli w kategoriach poleceń SQL ani nie wie, że istnieją tabele bazy danych o nazwach wp_posts i wp_users.

Dlatego nie musimy zbytnio martwić się o diagram bazy danych podczas tworzenia schematu GraphQL dla WordPress. Co więcej, możemy stworzyć schemat GraphQL, który naprawia część długu technicznego modelu danych WordPress.

Mapowanie modelu danych WordPress jako schematu GraphQL

Przystąpmy do mapowania. Najpierw mapujemy oryginalne encje jako typy, w miarę możliwości. Z listy encji w modelu danych WordPress tworzymy następujące typy dla schematu GraphQL:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

Następnie dodajemy wszystkie oczekiwane pola do każdego typu. Aby reprezentować schemat, możemy użyć SDL, czyli Schema Definition Language. (Jest to używane wyłącznie do celów dokumentacyjnych; sam plugin nie używa SDL do kodowania schematu: wszystko jest kodem PHP).

Oto pola (wśród wielu innych) dla Post:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
  date: Date!
}

Oto pola (wśród wielu innych) dla User:

type User {
  id: ID!
  name: String
  email: String!
}

Tworzymy również odpowiednie połączenia, czyli pola, które zwracają inną encję (zamiast skalara, takiego jak liczba lub ciąg znaków). Na przykład reprezentujemy post mający autora i użytkownika posiadającego posty:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

Pola i połączenia mogą również przyjmować argumenty. Na przykład umożliwiamy formatowanie Post.dateStr oraz filtrowanie wpisów, ograniczanie ich liczby i sortowanie w User.posts:

type Post {
  dateStr(format: String): Date!
}
 
type User {
  posts(
    filter: RootPostsFilterInput
    pagination: PostPaginationInput
    sort: CustomPostSortInput
  ): [Post!]!
}
 
input RootPostsFilterInput {
  authorIDs: [ID!]
  authorSlug: String
  categoryIDs: [ID!]
  dateQuery: [DateQueryInput!]
  excludeAuthorIDs: [ID!]
  excludeIDs: [ID!]
  hasPassword: Boolean = false
  ids: [ID!]
  isSticky: Boolean
  metaQuery: [CustomPostMetaQueryInput!]
  password: String
  search: String
  status: [FilterCustomPostStatusEnum!]
  tagIDs: [ID!]
  tagSlugs: [String!]
}
 
input PostPaginationInput {
  limit: Int
  offset: Int
}
 
input CustomPostSortInput {
  by: CustomPostOrderByEnum
  order: OrderEnum
}
 
# ...

Kontynuujemy ten proces dla wszystkich encji w modelu danych WordPress. Po zakończeniu dotrzemy do schematu GraphQL dla WordPress, widocznego za pomocą klienta Voyager (dostępnego jako "Interactive Schema" w menu pluginu):

Schemat GraphQL dla WordPress

Ten schemat ma podobieństwa do diagramu bazy danych WordPress, ale wykazuje też kilka różnic. Przeanalizujmy je.

Operacje bez encji są mapowane jako pola Root

Diagram bazy danych WordPress przedstawia sposób przechowywania danych, więc nie ma żadnego "początku". GraphQL jest jednak interfejsem do pobierania danych, dlatego musi istnieć etap początkowy, od którego można wykonywać queries.

Tym etapem początkowym jest typ Root, a dokładniej typy QueryRoot i MutationRoot (do obsługi queries i mutations odpowiednio).

W tych dwóch typach mapujemy wszystkie operacje, które nie zależą od encji, takie jak wykonywanie get_posts(), get_users() lub wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  loginUser(
    usernameOrEmail: String!,
    password: String!
  ): User
}

Pola nie muszą mieć tej samej nazwy ani sygnatury co operacja, którą reprezentują. Na przykład wywołanie pola loginUser może być uważane za bardziej odpowiednie niż signOn.

Grupowanie elementów schematu

Możemy stosować ulepszenia, aby uprościć schemat i uczynić go bardziej użytecznym. Na przykład pole może przyjmować wszystkie swoje argumenty za pośrednictwem obiektu input, który może być ponownie używany w kilku polach i ułatwia wizualizację schematu:

type MutationRoot {
  loginUser(input: LoginUserByInput!): User
}
 
input LoginUserByInput {
    usernameOrEmail: String!,
    password: String!
}

Ponadto odpowiedź z mutation może być obiektem "payload", który oprócz zwracania dotkniętego obiektu może zawierać również status operacji i komunikaty o błędach:

type MutationRoot {
  loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
type RootLoginUserMutationPayload {
  errors: [RootLoginUserMutationErrorPayloadUnion!]
  status: OperationStatusEnum!
  user: User
  userID: ID
}
 
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
  | InvalidUserEmailErrorPayload
  | InvalidUsernameErrorPayload
  | PasswordIsIncorrectErrorPayload
  | UserIsLoggedInErrorPayload

Wszystkie mutations trafiają pod MutationRoot

Istnieją operacje, które zależą od encji, takie jak wp_update_post(), która jest stosowana do jakiegoś posta. Odpowiadająca mutation w schemacie GraphQL musi być dodana do typu MutationRoot, ponieważ tak działa GraphQL.

Wtedy ta operacja jest mapowana w następujący sposób:

type MutationRoot {
  updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
 
input RootUpdatePostFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  id: ID!
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Ten plugin obsługuje również zagnieżdżone mutations, oferowane jako funkcja opt-in (ponieważ nie jest to standardowe zachowanie GraphQL). Dzięki temu mutations mogą być dodawane pod dowolnym typem, nie tylko MutationRoot. W takim przypadku otrzymujemy:

type Post {
  update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
 
input PostUpdateFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Zwróć uwagę na różnicę między inputami RootUpdatePostFilterInput a PostUpdateFilterInput (czyli między mutations z roota a mutations zagnieżdżonymi): pierwszy ma obowiązkową właściwość id, aby wskazać, który post zmodyfikować, ale drugi jej nie ma, ponieważ nie jest mu potrzebna.

Obsługa custom posts

W GraphQL nie ma dziedziczenia typów. Dlatego nie możemy mieć typu CustomPost i deklarować, że Post i Page go rozszerzają.

GraphQL oferuje dwa zasoby, aby zrekompensować ten brak: interfejsy i typy union.

W przypadku pierwszego tworzymy interfejs CustomPost dla schematu, deklarując wszystkie pola oczekiwane od custom posta, i definiujemy typy Post, Page i GenericCustomPost (reprezentujące wszystkie custom post types zdefiniowane przez dowolny zainstalowany motyw i plugin), aby implementowały ten interfejs:

interface CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Post implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Page implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type GenericCustomPost implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}

W przypadku drugiego tworzymy typ CustomPostUnion dla schematu, zwracający wszystkie custom post types:

union CustomPostUnion = Post | Page | GenericCustomPost

I sprawiamy, że pola zwracają ten typ, gdy jest to właściwe:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Podczas wykonywania queries możemy wybierać pola na podstawie rzeczywistego typu, takiego jak Post, lub na podstawie interfejsu CustomPost:

{  
  customPosts {
    __typename
    ...on CustomPost {
      id
      title
      slug
      status
    }
    ...on Post {
      isSticky
      postFormat
    }
  }
}

Jak można zauważyć, w schemacie GraphQL musimy wyraźnie zaznaczyć, kiedy mamy do czynienia z postami, a kiedy z custom postami, ponieważ to nie to samo! Używanie tych dwóch wymiennie to dług techniczny WordPress, który plugin stara się naprawić wszędzie tam, gdzie jest to możliwe.

Z tego powodu custom post jest zawsze nazywany CustomPost, a nie Post, pole obsługujące custom posty jest zawsze nazywane customPosts, a nie posts, a argument pola przyjmujący ID custom posta jest nazywany customPostID, a nie postID (nawet jeśli tak jest nazywany w mapowanej funkcji WordPress).

Dzięki temu oczekiwania są zawsze jasne:

  • Pole User.customPosts może zwrócić listę dowolnych custom postów, w tym postów i stron, a User.posts zwraca tylko posty
  • Pole Root.setFeaturedImageOnCustomPost może dodać obrazek wyróżniający do dowolnego custom posta, dlatego nie jest nazywane setFeaturedImageOnPost

Niegrupowanie tagów (i kategorii) pod jednym typem

Dlaczego typ PostTag (i analogicznie PostCategory) nosi taką nazwę, zamiast po prostu Tag?

Ponieważ podczas wykonywania tej queries (gdzie produkt jest CPT), wyniki pola tags dla postów i produktów zawsze będą różne i niepokrywające się:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

Tagi dodane do postów nie pojawią się podczas pobierania tagów produktów i odwrotnie (chyba że produkt używa również taksonomii post_tag, ale wtedy może być też reprezentowany typem PostTag). Nie stanowi to dużego problemu w WordPress, ponieważ te elementy można traktować jako różne wiersze z tej samej tabeli bazy danych. Ale ma to znaczenie dla GraphQL, który jest silnie typowany.

Dlatego dobrą decyzją projektową jest utrzymywanie tych encji oddzielnie, pod własnymi typami, i zwracanie tagów postów pod typem PostTag, a jeśli niestandardowy plugin implementuje własny CPT produktu, musi używać dla swoich tagów typu ProductTag.

Nadawanie elementom mediów własnej tożsamości

Encje mediów w WordPress są custom post types wyłącznie dlatego, że było to wygodne z punktu widzenia implementacji. Jednak schemat GraphQL może uniknąć tego długu technicznego i modelować elementy mediów jako odrębną encję, a nie jako custom posty.

Oznacza to następujące decyzje dla schematu GraphQL:

  • Typ Media nie implementuje interfejsu CustomPost i nie będzie częścią typu CustomPostUnion
  • Typ Media nie posiada wielu pól oczekiwanych od custom post type, takich jak excerpt, date i status. Zamiast tego ma tylko pola oczekiwane od elementu mediów:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Identyfikowanie i mapowanie enumów

W niektórych sytuacjach WordPress używa stałych wartości z określonego zestawu. Na przykład status posta może wynosić tylko "publish", "draft", "pending" lub "trash".

W GraphQL możemy traktować je jako enumy (zamiast stringów) i tworzyć odpowiadający typ wyliczeniowy. Zgodnie ze standardem GraphQL, enumy powinny być pisane wielkimi literami, tak:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Jednak wtedy queries nie można bezpośrednio używać do interakcji z WordPress, ponieważ wykonanie get_posts( [ "post_status" => "PUBLISH" ] ) nie działa.

Dlatego jako kompromis zachowujemy te wartości enumów małymi literami:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Mapowanie dodatkowych typów

Bloki nie są bezpośrednio widoczne w diagramie bazy danych WordPress, ponieważ są przechowywane w wp_posts (nie istnieje tabela wp_blocks), ale mimo to są odrębną encją.

Dlatego możemy wprowadzić typ Block, aby je zmapować:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}