Kavramlar, Fikirler, Stratejiler
Kavramlar, Fikirler, StratejilerPlugin WordPress veri modelini GraphQL şemasına nasıl eşler

Plugin WordPress veri modelini GraphQL şemasına nasıl eşler

Gato GraphQL'in WordPress veri modelini ilgili bir GraphQL şemasına nasıl eşlediği aşağıda açıklanmaktadır.

WordPress veri modeli

WordPress'in şu varlıkları bulunmaktadır:

  • posts
  • pages
  • custom posts
  • medya öğeleri
  • kullanıcılar
  • kullanıcı rolleri
  • etiketler
  • kategoriler
  • yorumlar
  • bloklar
  • meta özellikler
  • diğerleri (seçenekler, eklentiler, temalar, vb.)

Bu varlıklar hiyerarşik bir yapıya sahip olabilir. Örneğin, post, page ve medya öğelerinin hepsi custom post type'lardır; etiketler ve kategoriler ise birer taksonomidir.

Tüm varlıklara ait verilerin nasıl depolandığını gösteren WordPress veritabanı diyagramı aşağıdadır:

WordPress veritabanı diyagramı

Eşleme, veritabanı diyagramının birebir kopyası mı?

WordPress veritabanını bir GraphQL şemasına eşlerken, yukarıdaki diyagram 1'e 1 olarak mı gözetiliyor?

Hayır, gözetilmiyor. Veritabanı diyagramı gerçek bir uygulama olsa da GraphQL, istemciden verilere erişmek için kullanılan bir arayüzdür. Bu ikisi birbiriyle ilişkilidir, ancak farklı olabilirler. GraphQL veritabanıyla ilgilenmez: SQL komutları cinsinden düşünmez ve wp_posts ile wp_users adlı veritabanı tablolarının var olduğunu bilmez.

Bu nedenle, WordPress için GraphQL şeması oluştururken veritabanı diyagramı hakkında çok fazla endişelenmemize gerek yoktur. Hatta WordPress veri modelindeki bazı teknik borçları gideren bir GraphQL şeması üretebiliriz.

WordPress veri modelini GraphQL şeması olarak eşleme

Eşlemeyi yapalım. Öncelikle, mümkün olduğunca orijinal varlıkları type olarak eşliyoruz. WordPress veri modelindeki varlık listesinden, GraphQL şeması için şu type'ları üretiyoruz:

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

Ardından, her type'a beklenen tüm alanları ekliyoruz. Şemayı temsil etmek için SDL yani Schema Definition Language kullanabiliriz. (Bu yalnızca belgeleme amaçlıdır; eklentinin kendisi şemayı kodlamak için SDL kullanmaz: her şey PHP kodudur.)

Bir Post için alanlar (diğerleri arasında) şunlardır:

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

Bir User için alanlar (diğerleri arasında) şunlardır:

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

Ayrıca, skaler (sayı veya dize gibi) yerine başka bir varlık döndüren alanlar olan ilgili bağlantıları da oluştururuz. Örneğin, bir postun bir yazarı olduğunu ve bir kullanıcının postlara sahip olduğunu şu şekilde temsil ederiz:

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

Alanlar ve bağlantılar ayrıca argüman da kabul edebilir. Örneğin, Post.dateStr alanının biçimlendirilebilmesini ve User.posts alanının girişleri filtreleyip sayısını sınırlandırıp sıralayabilmesini sağlarız:

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
}
 
# ...

WordPress veri modelindeki tüm varlıklar için bunu yapmaya devam ederiz. İşimiz bittiğinde, eklentinin menüsünde "Interactive Schema" olarak erişilebilen Voyager istemcisi aracılığıyla görülebilen WordPress için GraphQL şemasına ulaşırız:

WordPress için GraphQL şeması

Bu şema, WordPress veritabanı diyagramıyla bazı benzerlikler taşısa da çeşitli farklılıklar da içermektedir. Gelin bunları inceleyelim.

Varlığı olmayan işlemler Root alanları olarak eşlenir

WordPress veritabanı diyagramı verilerin nasıl depolandığını gösterir, dolayısıyla bir "başlangıç" noktası yoktur. GraphQL ise veri almak için bir arayüz olduğundan, sorgunun çalıştırılacağı bir başlangıç aşaması bulunması gerekir.

Bu başlangıç aşaması Root type'ı, daha kesin bir ifadeyle QueryRoot ve MutationRoot type'larıdır (sırasıyla queries ve mutation'ları ele almak için).

Bu iki type'a, get_posts(), get_users() veya wp_signon() çalıştırılırken olduğu gibi bir varlığa bağımlı olmayan tüm işlemleri eşleriz:

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

Alanların temsil ettikleri işlemle aynı adı veya imzayı taşıması gerekmez. Örneğin, loginUser alanını çağırmak signOn yerine daha uygun görülebilir.

Şema öğelerini gruplandırma

Şemayı basitleştirmek ve daha kullanışlı hale getirmek için iyileştirmeler uygulayabiliriz. Örneğin, bir alan tüm argümanlarını bir input nesnesi aracılığıyla alabilir; bu nesne birden fazla alanda yeniden kullanılabilir ve şemanın görselleştirilmesini kolaylaştırır:

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

Buna ek olarak, bir mutation'ın yanıtı, etkilenen nesneyi döndürmenin yanı sıra işlemin durumunu ve hata mesajlarını da içerebilen bir "payload" nesnesi olabilir:

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

Tüm mutation'lar MutationRoot altına gider

wp_update_post() gibi bir varlığa bağımlı olan işlemler de vardır; bu işlev belirli bir posta uygulanır. GraphQL'in çalışma biçimi gereği, GraphQL şemasındaki ilgili mutation MutationRoot type'ına eklenmek zorundadır.

Bu durumda işlem şu şekilde eşlenir:

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

Bu eklenti aynı zamanda iç içe mutation'ları da desteklemektedir; bu özellik opt-in olarak sunulmaktadır (standart GraphQL davranışı olmadığı için). Böylece mutation'lar yalnızca MutationRoot altına değil, herhangi bir type altına da eklenebilir. Bu durumda şunu elde ederiz:

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

RootUpdatePostFilterInput ile PostUpdateFilterInput arasındaki farka dikkat edin (yani kökten gelen mutation'lar ile iç içe mutation'lar arasındaki fark): birincisinde hangi postun değiştirileceğini belirtmek için zorunlu id özelliği bulunurken ikincisinde bulunmaz, çünkü buna ihtiyacı yoktur.

Custom post'larla başa çıkma

GraphQL'de type kalıtımı yoktur. Bu nedenle, bir CustomPost type'ına sahip olup Post ve Page'in onu genişlettiğini bildiremeyiz.

GraphQL bu eksikliği telafi etmek için iki kaynak sunar: interface'ler ve union type'lar.

Birincisi için şemada bir CustomPost interface'i oluştururuz; bu interface, bir custom post'tan beklenen tüm alanları bildirir ve Post, Page ile GenericCustomPost type'larını (yüklü herhangi bir tema ve eklenti tarafından tanımlanan tüm custom post type'larını temsil etmek için) bu interface'i uygulayacak şekilde tanımlarız:

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!
}

İkincisi için şemada tüm custom post type'larını döndüren bir CustomPostUnion type'ı oluştururuz:

union CustomPostUnion = Post | Page | GenericCustomPost

Ve uygun olduğunda alanların bu type'ı döndürmesini sağlarız:

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

Sorguyu çalıştırırken alanları Post gibi gerçek type'a ya da CustomPost interface'ine göre seçebiliriz:

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

Görüldüğü üzere, GraphQL şemasında post'larla mı yoksa custom post'larla mı ilgilendiğimizi açıkça belirtmemiz gerekir; zira bunlar aynı şey değildir! Bu ikisini birbirinin yerine kullanmak, eklentinin mümkün olan her durumda düzeltmeye çalıştığı bir WordPress teknik borcudur.

Bu nedenle, bir custom post her zaman Post değil CustomPost olarak adlandırılır; custom post'larla ilgilenen bir alan her zaman posts değil customPosts olarak adlandırılır ve bir custom post'un ID'sini alan bir alan argümanı, eşlenen WordPress işlevinde bu şekilde çağrılsa bile postID değil customPostID olarak adlandırılır.

Böylece beklenti her zaman açıktır:

  • User.customPosts alanı, post'lar ve sayfalar dahil olmak üzere herhangi bir custom post listesi döndürebilirken User.posts yalnızca post'ları döndürür
  • Root.setFeaturedImageOnCustomPost alanı herhangi bir custom post'a öne çıkan görsel ekleyebilir; bu yüzden setFeaturedImageOnPost olarak adlandırılmamıştır

Etiketleri (ve kategorileri) tek bir type altında gruplandırmamak

PostTag type'ı (aynı durum PostCategory için de geçerlidir) neden Tag yerine bu şekilde adlandırılmıştır?

Çünkü bu sorguyu çalıştırırken (bir ürünün CPT olduğu durumda), post'lar ve ürünler için tags alanından gelen sonuçlar her zaman farklı ve örtüşmeyen sonuçlar olacaktır:

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

Post'lara eklenen etiketler, ürünlerin etiketleri alınırken görünmeyecek ve bunun tersi de geçerli olacaktır (bir ürün post_tag taksonomisin de kullanmadığı sürece; bu durumda PostTag type'ıyla da temsil edilebilir). Bu durum WordPress'te büyük bir sorun teşkil etmez, zira bu öğeler aynı veritabanı tablosunun farklı satırları olarak değerlendirilebilir. Ancak güçlü bir şekilde yazılmış olan GraphQL için önem taşır.

Bu nedenle, bu varlıkları ayrı tutmak ve kendi type'larına sahip kılmak, post etiketlerinin PostTag type'ı altında döndürülmesini sağlamak ve özel bir eklenti kendi ürün CPT'sini uyguluyorsa etiketleri için ProductTag type'ını kullanması gerektiğini belirtmek iyi bir tasarım kararıdır.

Medya öğelerine kendi kimliklerini vermek

WordPress'teki medya varlıkları, yalnızca uygulama açısından uygun olduğu için custom post type'larıdır. Ancak GraphQL şeması bu teknik borcu önleyebilir ve medya öğelerini custom post olarak değil, ayrı bir varlık olarak modelleyebilir.

Bu durum, GraphQL şeması için şu kararları beraberinde getirir:

  • Media type'ı CustomPost interface'ini uygulamaz ve CustomPostUnion type'ının parçası olmaz
  • Media type'ı, excerpt, date ve status gibi bir custom post type'dan beklenen pek çok alana sahip değildir. Bunun yerine, yalnızca bir medya öğesinden beklenen alanlara sahiptir:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Enum'ları tanımlama ve eşleme

Bazı durumlarda WordPress, belirli bir kümeden sabit değerler kullanır. Örneğin, bir postun durumu yalnızca "publish", "draft", "pending" veya "trash" olabilir.

GraphQL'de bunları (dize yerine) enum olarak ele alabilir ve ilgili bir numaralandırma type'ı oluşturabiliriz. GraphQL standardına göre enum'lar büyük harfle yazılmalıdır:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Ancak bu durumda sorgu, get_posts( [ "post_status" => "PUBLISH" ] ) çalıştırmanın işe yaramaması nedeniyle WordPress ile doğrudan etkileşim kurmak için kullanılamaz.

Bu yüzden bir uzlaşı olarak bu enum değerlerini küçük harfle tutarız:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Ek type'ları eşleme

Bloklar, WordPress veritabanı diyagramında doğrudan görünmez; zira wp_posts içinde depolanırlar (wp_blocks adlı bir tablo yoktur). Bununla birlikte, yine de ayrı bir varlıktır.

Dolayısıyla bunları eşlemek için bir Block type'ı tanıtabiliriz:

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