Blog

👶🏻 Trẻ hóa WordPress thông qua GraphQL

Leonardo Losoviz
Bởi Leonardo Losoviz ·

WordPress là một CMS truyền thống: được phát minh từ hơn 17 năm trước, nó chứa đầy code PHP mà nếu được viết lại, sẽ được triển khai theo cách khác.

GraphQL là một giao diện hiện đại để truy cập dữ liệu. Hãy chú ý đến từ "giao diện": nó không quan tâm hệ thống dữ liệu bên dưới được triển khai như thế nào, mà chỉ quan tâm đến cách phơi bày dữ liệu ra ngoài.

Điều gì xảy ra khi chúng ta kết hợp hai thứ này? Chúng ta nên thiết kế giao diện GraphQL để truy cập dữ liệu từ WordPress như thế nào?

Có một vài chiến lược rõ ràng mà chúng ta có thể áp dụng:

  1. Tôn trọng truyền thống, cung cấp một ánh xạ giữ nguyên mô hình dữ liệu WordPress như hiện tại, bao gồm cả khoản nợ kỹ thuật tích lũy qua nhiều năm

  2. Sửa chữa khoản nợ kỹ thuật, cung cấp một giao diện phơi bày dữ liệu theo cách trừu tượng, không nhất thiết phải gắn với WordPress

Cả hai cách tiếp cận đều có ưu và nhược điểm, không có cái nào đúng hay sai hoàn toàn. Đó chỉ là quan điểm cá nhân, ưu tiên hành vi này hơn hành vi kia.

Đối với plugin Gato GraphQL tôi đã chọn cách tiếp cận thứ hai, cố gắng tạo ra một schema GraphQL mà, dù dựa trên WordPress và hoạt động cho WordPress, nhưng không bị ràng buộc với WordPress (chẳng hạn, bằng cách loại bỏ các tên và mối quan hệ không nhất quán).

Kết quả là GraphQL trẻ hóa WordPress: trong khi chúng ta vẫn có WordPress làm CMS nền tảng, với code PHP truyền thống của nó, lớp dữ liệu có thể được tạo lại từ đầu, dựa trên lẽ thường, không phải truyền thống. Lớp dữ liệu quay trở lại từ tuổi thiếu niên, để trở thành trẻ thơ một lần nữa.

GraphQL + WordPress rock

Kết quả là một schema GraphQL đại diện cho mô hình dữ liệu WordPress, đồng thời hỗ trợ các nested mutation.

Hãy cùng xem cách nó được thực hiện.

Mô hình dữ liệu WordPress

WordPress có các thực thể sau:

  • bài viết (posts)
  • trang (pages)
  • custom posts
  • phần tử media
  • người dùng (users)
  • vai trò người dùng (user roles)
  • thẻ (tags)
  • danh mục (categories)
  • bình luận (comments)
  • block
  • thuộc tính meta
  • khác (tùy chọn, plugin, theme, v.v.)

Các thực thể này có thể có phân cấp. Ví dụ, post, page và phần tử media đều là custom post type, còn tags và categories đều là taxonomy.

Đây là sơ đồ cơ sở dữ liệu WordPress, hiển thị cách dữ liệu cho tất cả các thực thể được lưu trữ:

Sơ đồ cơ sở dữ liệu WordPress

Ánh xạ có phải là bản sao chính xác của sơ đồ DB không?

Khi ánh xạ cơ sở dữ liệu WordPress vào một schema GraphQL, sơ đồ ở trên có được tuân theo 1 đối 1 không?

Không, không phải vậy. Trong khi sơ đồ cơ sở dữ liệu là một triển khai thực tế, GraphQL là giao diện để truy cập dữ liệu từ phía client. Hai thứ này có liên quan, nhưng có thể khác nhau. GraphQL không quan tâm đến cơ sở dữ liệu: nó không nghĩ theo lệnh SQL, hay biết rằng có các bảng cơ sở dữ liệu tên là wp_postswp_users.

Vì vậy chúng ta không cần lo lắng quá nhiều về sơ đồ cơ sở dữ liệu khi tạo schema GraphQL cho WordPress. Điều đó có nghĩa là chúng ta có thể tạo ra một schema GraphQL sửa chữa một số khoản nợ kỹ thuật từ mô hình dữ liệu WordPress.

Ánh xạ mô hình dữ liệu WordPress thành schema GraphQL

Hãy thực hiện việc ánh xạ. Đầu tiên, chúng ta ánh xạ các thực thể gốc thành các kiểu dữ liệu (types), càng nhiều càng tốt. Từ danh sách các thực thể trong mô hình dữ liệu WordPress, chúng ta tạo ra các types sau cho schema GraphQL:

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

Sau đó, chúng ta thêm tất cả các field dự kiến vào mỗi type. Để biểu diễn schema, chúng ta có thể dùng SDL, hay Schema Definition Language. (Điều này chỉ được dùng cho mục đích tài liệu; bản thân plugin không dùng SDL để mã hóa schema: tất cả đều là code PHP).

Đây là các field (trong số nhiều field khác) cho một Post:

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

Đây là các field (trong số nhiều field khác) cho một User:

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

Chúng ta cũng tạo các connection tương ứng, là các field trả về một thực thể khác (thay vì scalar như số hoặc chuỗi). Ví dụ, chúng ta biểu diễn một post có tác giả, và một user sở hữu các post:

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

Các field và connection cũng có thể chấp nhận đối số. Ví dụ, chúng ta cho phép Post.date được định dạng, và User.posts tìm kiếm các mục và giới hạn số lượng của chúng:

type Post {
  date(format: String): Date!
}
 
type User {
  posts(limit: Int, search: String): [Post]
}

Chúng ta tiếp tục làm điều này cho tất cả các thực thể trong mô hình dữ liệu WordPress. Khi hoàn thành, chúng ta sẽ đến được schema GraphQL cho WordPress, có thể xem bằng Voyager client (có sẵn dưới dạng "Interactive Schema" trong menu của plugin):

Schema GraphQL cho WordPress

Schema này có những điểm tương đồng với sơ đồ cơ sở dữ liệu WordPress, nhưng cũng có nhiều điểm khác biệt. Hãy phân tích chúng.

Các thao tác không có thực thể được ánh xạ thành Root fields

Sơ đồ cơ sở dữ liệu WordPress biểu diễn cách dữ liệu được lưu trữ, do đó không có "điểm bắt đầu". Tuy nhiên, GraphQL là giao diện để lấy dữ liệu, do đó phải có giai đoạn ban đầu để thực thi query.

Giai đoạn ban đầu này là type Root, hay chính xác hơn là các type QueryRootMutationRoot (để xử lý queries và mutations tương ứng).

Trong hai type này, chúng ta ánh xạ tất cả các thao tác không phụ thuộc vào một thực thể, chẳng hạn khi thực thi get_posts(), get_users() hay wp_signon():

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

Các field không cần có cùng tên hay chữ ký với thao tác mà chúng đại diện. Ví dụ, gọi field logUserIn có thể được coi là phù hợp hơn signOn.

Tất cả mutations đều nằm dưới MutationRoot

Có những thao tác phụ thuộc vào một thực thể, chẳng hạn wp_update_post(), được áp dụng lên một post nào đó. Mutation tương ứng trong schema GraphQL phải được thêm vào type MutationRoot, vì đó là cách GraphQL hoạt động.

Khi đó, thao tác này được ánh xạ như sau:

type MutationRoot {
  updatePost(input: {
    postID: ID!,
    newTitle: String,
    newContent: String
  }): Post
}

Plugin này cũng hỗ trợ nested mutations, được cung cấp như một tính năng opt-in (vì đây không phải hành vi GraphQL tiêu chuẩn). Khi đó, mutations cũng có thể được thêm vào bất kỳ type nào, không chỉ MutationRoot. Trong trường hợp này, chúng ta thu được:

type Post {
  update(input: {
    newTitle: String,
    newContent: String
  }): Post!
}

Xử lý custom posts

Không có kế thừa type trong GraphQL. Do đó, chúng ta không thể có type CustomPost, và khai báo rằng PostPage mở rộng từ nó.

GraphQL cung cấp hai cơ chế để bù đắp cho sự thiếu hụt này: interfaces và union types.

Đối với cái đầu tiên, chúng ta tạo một interface CustomPost cho schema, khai báo tất cả các field dự kiến từ một custom post, và định nghĩa các type PostPage để triển khai interface:

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

Đối với cái thứ hai, chúng ta tạo type CustomPostUnion cho schema trả về tất cả các custom post type:

union CustomPostUnion = Post | Page

Và để các field trả về type này khi phù hợp:

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

Như có thể thấy, trong schema GraphQL chúng ta cần khẳng định rõ ràng khi nào đang làm việc với posts, và khi nào với custom posts, vì chúng không giống nhau! Gọi lẫn lộn hai khái niệm này là khoản nợ kỹ thuật từ WordPress, mà chúng ta có thể sửa chữa.

Vì lý do đó, một custom post luôn được gọi là CustomPost chứ không phải Post, một field xử lý custom posts luôn được gọi là customPosts chứ không phải posts, và một đối số field nhận ID cho một custom post được gọi là customPostID chứ không phải postID (dù đó là cách nó được gọi trong hàm WordPress được ánh xạ).

Khi đó, kỳ vọng luôn rõ ràng:

  • field User.customPosts có thể trả về danh sách bất kỳ custom post nào, bao gồm posts và pages, còn User.posts chỉ trả về posts
  • field Root.setFeaturedImageOnCustomPost có thể thêm ảnh đại diện vào bất kỳ custom post nào, đó là lý do nó không được gọi là setFeaturedImageOnPost

Không gộp tags (và categories) dưới một type duy nhất

Tại sao type PostTag (và tương tự với PostCategory) lại được gọi như vậy, thay vì chỉ là Tag?

Vì khi thực thi query này (trong đó một sản phẩm là một CPT), kết quả từ field tags cho posts và sản phẩm sẽ luôn khác nhau, không chồng chéo:

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

Các tag được thêm vào posts sẽ không xuất hiện khi lấy tags cho sản phẩm, và ngược lại (trừ khi một sản phẩm cũng dùng taxonomy post_tag, nhưng khi đó nó cũng có thể được biểu diễn với type PostTag). Điều này không phải vấn đề lớn trong WordPress, vì các mục này có thể được coi là các hàng khác nhau trong cùng một bảng cơ sở dữ liệu. Nhưng nó có ý nghĩa đối với GraphQL, vốn được định kiểu chặt chẽ.

Do đó, đây là quyết định thiết kế tốt khi giữ các thực thể này tách biệt, dưới các type riêng của chúng, và để tags cho posts được trả về dưới type PostTag, và nếu một plugin tùy chỉnh triển khai CPT sản phẩm riêng, nó phải dùng type ProductTag cho tags của nó.

Cho phần tử media có danh tính riêng

Các thực thể media trong WordPress là custom post type, chỉ vì điều đó thuận tiện từ góc độ triển khai. Tuy nhiên, schema GraphQL có thể tránh khoản nợ kỹ thuật này, và mô hình hóa các phần tử media như một thực thể riêng biệt, không phải là custom post.

Điều này dẫn đến các quyết định sau cho schema GraphQL:

  • Khi truy vấn field customPosts, nó sẽ không lấy các phần tử media
  • Type Media không triển khai interface CustomPost, và sẽ không là một phần của type CustomPostUnion
  • Type Media không có nhiều field dự kiến từ một custom post type, chẳng hạn excerpt, datestatus. Thay vào đó, nó chỉ có những field dự kiến từ một phần tử media:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Nhận diện và ánh xạ enums

Trong một số tình huống, WordPress sử dụng các giá trị cố định từ một tập hợp nhất định. Ví dụ, trạng thái của một post chỉ có thể là "publish", "draft", "pending" hay "trash".

Trong GraphQL, chúng ta có thể xử lý chúng như enums (thay vì chuỗi), và tạo một kiểu liệt kê tương ứng. Theo chuẩn GraphQL, enums nên được viết bằng chữ hoa, như sau:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Tuy nhiên, khi đó query không thể được dùng trực tiếp để tương tác với WordPress, vì thực thi get_posts( [ "post_status" => "PUBLISH" ] ) không hoạt động.

Vì vậy, như một sự thỏa hiệp, chúng ta giữ các giá trị enum này bằng chữ thường:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Ánh xạ các type bổ sung

Blocks không trực tiếp hiển thị trong sơ đồ cơ sở dữ liệu WordPress, vì chúng được lưu trữ trong wp_posts (không có bảng wp_blocks), nhưng dù vậy chúng vẫn là một thực thể riêng biệt.

Do đó, chúng ta giới thiệu type Block để ánh xạ chúng:

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

Đă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.