Blog

👨🏻‍💻 GraphQL như là một (kiểu) ngôn ngữ lập trình

Leonardo Losoviz
Bởi Leonardo Losoviz ·

GraphQL, mặc dù có ngôn ngữ GraphQL, thông thường sẽ không được gọi là ngôn ngữ lập trình, vì có rất nhiều thứ chúng ta có thể làm với ngôn ngữ lập trình mà chúng ta không thể làm với GraphQL.

GraphQL thường được dùng để lấy dữ liệu, ví dụ để render một trang web trên client, và để thay đổi dữ liệu, ví dụ để tạo một bài viết. Và đó là gần như tất cả.

(Các cách sử dụng khác chỉ đơn giản là sự kết hợp của 2 trường hợp trước. Ví dụ, một API gateway có thể lấy/thay đổi dữ liệu từ một server nội bộ, không được tiếp xúc với client.)

Truy cập dữ liệu trong GraphQL:

query PrintPostTitle($postID: ID!)
{
  post(by: { id: $postID }) {
    title
  }
}

...có phần tương đương (kiểu vậy) trong PHP như sau:

function printPostTitle(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
}

(Tất cả các ví dụ dưới đây sẽ sử dụng PHP là ngôn ngữ lập trình để so sánh.)

Thay đổi dữ liệu trong GraphQL:

query UpdatePost($postID: ID!, $title: String!)
{
  updatePost(
    by: { id: $postID },
    input: { title: $title }
  ) {
    title
  }
}

...có phần tương đương (kiểu vậy) trong PHP như sau:

function updatePost(int $postID, string $title)
{
  $post = getPost($postID);
  $post->update(['title' => $title]);
}

Điều này là đủ vì GraphQL thường được truy cập từ một client (được viết bằng ngôn ngữ lập trình nào đó, chẳng hạn JavaScript, PHP, Java, hoặc ngôn ngữ khác) mà sẽ chứa logic về việc làm gì với dữ liệu. Vì vậy GraphQL không được dùng một mình, mà là đồng hành với ai đó.

Nhưng nếu GraphQL có thể được dùng một mình, thì nhiều trường hợp sử dụng mới có thể được giải quyết chỉ với GraphQL, cho phép GraphQL được triển khai trong các môi trường mới và chịu trách nhiệm cho các tác vụ bổ sung trong ngăn xếp ứng dụng.

Tuy nhiên, để điều đó xảy ra, GraphQL phải hỗ trợ nhiều tính năng của ngôn ngữ lập trình.

Các tính năng ngôn ngữ lập trình mà GraphQL hỗ trợ còn hạn chế. Ví dụ, sử dụng directive @include (hoặc @skip) và truyền một biến làm đầu vào có thể được coi là (kiểu vậy) logic điều kiện:

query PrintPostProperties($postID: ID!, $addContent: Boolean!)
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

Query này có phần tương đương PHP như sau:

function printPostProperties(int $postID, bool $addContent)
{
  $post = getPost($postID);
  echo $post->title;
  if ($addContent) {
    echo $post->content;
  }
}

Đó là gần như tất cả. GraphQL thiếu đệ quy, biến động (nơi giá trị của chúng được tính toán và gán cho biến lúc runtime, không phải là đầu vào trong từ điển), gán biến (ví dụ: gán đầu ra của một trường cho một biến, sau đó có thể được cung cấp làm đối số vào một trường khác), và những thứ khác.

Hãy xem xét cách bạn sẽ triển khai một giải pháp, chỉ dùng GraphQL, cho vấn đề sau:

  • Tạo một webhook để được gọi bởi một dịch vụ mỗi khi một người dùng mới đăng ký dịch vụ đó; người dùng có thể đã đăng ký nhận bản tin (được chỉ ra bởi trường marketing_optin trong payload của webhook); trong trường hợp đó, webhook phải đăng ký email của người dùng (trong trường email của payload webhook) vào một danh sách Mailchimp.

Bạn có cho rằng điều đó có thể thực hiện không? dễ? khó? không thể?

Tại Gato GraphQL, chúng tôi muốn giải quyết vấn đề này chỉ bằng GraphQL. Và rất nhiều vấn đề khác nữa. Đó là lý do tại sao chúng tôi đã suy nghĩ kỹ về cách hỗ trợ các đặc điểm từ ngôn ngữ lập trình.

Hãy cùng khám phá những tính năng lập trình nào chúng tôi đã hỗ trợ trên máy chủ GraphQL của mình. Ở cuối bài viết này, chúng ta sẽ thấy cách giải quyết vấn đề đó.

Chức năng

Các trường trong GraphQL thường mang dữ liệu, chẳng hạn như tiêu đề, nội dung hoặc dữ liệu của một bài viết. Nhưng chúng ta cũng có thể triển khai các trường như là "chức năng".

Ví dụ, in ra thời gian trong PHP:

function printTime()
{
  echo time();
}

...có thể được thực hiện với trường _time trong GraphQL:

{
  _time
}

Lưu ý rằng hàm time không thuộc bất kỳ kiểu nào, do đó trường _time cũng không. Như vậy, nó là một trường toàn cục, và có thể được truy cập dưới mọi kiểu từ schema GraphQL:

{
  posts {
    _time
  }
}

Các ví dụ khác về trường chức năng là:

  • _arrayItem
  • _arrayJoin
  • _date
  • _equals
  • _inArray
  • _intAdd
  • _isEmpty
  • _isNull
  • _makeTime
  • _objectProperty
  • _sprintf
  • _strContains
  • _strRegexReplace
  • _strSubstr

Hàm

Chúng ta có thể chia các đơn vị logic thành các hàm, và để một hàm gọi một hàm khác:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  printPostTitle();
  printPostContent();
}
 
function printPostTitle(Post $post)
{
  echo $post->title;
}
 
function printPostContent(Post $post)
{
  echo $post->content;
}

Trong GraphQL, chúng ta có thể tương tự chia thao tác query (hoặc mutation) trong tài liệu thành nhiều thao tác query, và để một thao tác "phụ thuộc" vào các thao tác khác, do đó thực thi chúng trước:

query PrintPostTitle($postID: ID!)
{
  postWithTitle: post(by: { id: $postID }) {
    title
  }
}
 
query PrintPostContent($postID: ID!)
{
  postWithContent: post(by: { id: $postID }) {
    content
  }
}
 
query PrintPostProperties
  @depends(on: [
    "PrintPostTitle",
    "PrintPostContent"
  ])
{
  # ...
}

Trong query này, thực thi query GraphQL khi truyền ?operationName=PrintPostProperties vào endpoint sẽ thực thi các queries PrintPostTitlePrintPostContent trước, và chỉ sau đó mới thực thi PrintPostProperties.

Điều này có thể thực hiện được nhờ Thực thi nhiều queries.

Biến động

Chúng ta có thể tính toán một giá trị và gán nó cho một biến lúc runtime. Sau đó, dựa trên giá trị đó, chúng ta có thể thực thi có điều kiện một số chức năng hoặc không:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
  
  $addContent = isUserLoggedIn();
  if ($addContent) {
    echo $post->content;
  }
}

Trong GraphQL, chúng ta có thể "xuất" một giá trị dưới một biến động trong một thao tác, và sau đó đọc giá trị này ở một thao tác khác:

query ExportAddContent
{
  addContent: isUserLoggedIn
    @export(as: "addContent")
}
 
query PrintPostProperties($postID: ID!)
  @depends(on: "ExportAddContent")
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

Lưu ý rằng biến $addContent, chứa giá trị được tính toán lúc runtime, được đọc bởi nhưng không được khai báo trong thao tác PrintPostProperties, vì nó là một biến động.

Thực thi hàm có điều kiện

Một phương án thay thế cho ví dụ trước là nhóm logic thành các hàm, và sau đó thực thi có điều kiện một hàm hoặc không tùy thuộc vào giá trị của biến động:

function printPostProperties(int $postID)
{
  $post = getPost($postID);
  printPostTitle();
  
  $addContent = isUserLoggedIn();
  if ($addContent) {
    printPostContent();
  }
}
 
function printPostTitle(Post $post)
{
  echo $post->title;
}
 
function printPostContent(Post $post)
{
  echo $post->content;
}

Trong GraphQL chúng ta có thể thêm directive @include vào thao tác:

query ExportAddContent
{
  addContent: isUserLoggedIn
    @export(as: "addContent")
}
 
query PrintPostTitle($postID: ID!)
{
  postWithTitle: post(by: { id: $postID }) {
    title
  }
}
 
query PrintPostContent($postID: ID!)
  @depends(on: "ExportAddContent")
  @include(if: $addContent)
{
  postWithContent: post(by: { id: $postID }) {
    content
  }
}
 
query PrintPostProperties
  @depends(on: [
    "PrintPostTitle",
    "PrintPostContent"
  ])
{
  # ...
}

Bây giờ, thao tác PrintPostContent chỉ được thực thi nếu $addContenttrue.

Gán biến, đưa chúng trở lại làm đầu vào

Hãy sửa đổi nhẹ ví dụ trước, trong đó điều kiện "addContent" gắn với việc người dùng đã đăng nhập hay chưa.

Trong ví dụ khác này, "addContent"true mỗi khi hôm nay là cuối tuần, điều này liên quan đến một số logic để tính toán:

  • Lấy ngày hôm nay
  • Định dạng nó thành tên ngày trong tuần, ở dạng chữ thường
  • Kiểm tra xem đó là "saturday" hay "sunday"

Trong PHP:

function addContent()
{
  $today = time();
  $dayName = date('l', $today);
  $lcDayName = strtolower($dayName);
  $isWeekend = in_array(
    $lcDayName,
    ['saturday', 'sunday']
  );
  return $isWeekend;
}
 
function printPostProperties(int $postID)
{
  $post = getPost($postID);
  echo $post->title;
 
  $addContent = addContent();
  if ($addContent) {
    echo $post->content;
  }
}

Trong GraphQL:

query ExportAddContent
{
  today: _time
  dayName: _date(format: "l", timestamp: $__today)
  lcDayName: _strLowerCase(text: $__dayName)
  isWeekend: _inArray(
    value: $__lcDayName
    array: ["saturday", "sunday"],
  )
    @export(as: "addContent")
}
 
query PrintPostProperties($postID: ID!)
  @depends(on: "ExportAddContent")
{
  post(by: { id: $postID }) {
    title
    content @include(if: $addContent)
  }
}

Trong thao tác ExportAddContent, giá trị của mỗi trường được truy vấn có sẵn ngay lập tức cho các trường bên dưới, dưới biến động $__fieldName. Theo cách này, đầu ra của một trường có thể được sử dụng ngay lập tức làm đầu vào cho một trường khác, ngay trong cùng một thao tác.

Điều này có thể thực hiện được nhờ Field to Input.

Sửa đổi giá trị một cách động

Trong ví dụ PHP này, chúng ta sửa đổi giá trị của một biến khi người dùng đăng nhập là admin, trong trường hợp đó nội dung bài viết được thêm một liên kết để chỉnh sửa bài viết:

function isAdminUser()
{
  $user = getCurrentUser();
  return in_array("administrator", $user->roles);
}
 
function printPostContent(int $postID)
{
  $post = getPost($postID);
  $postContent = $post->content;
 
  $isAdminUser = isAdminUser();
  if ($isAdminUser) {
    $postContent = sprintf(
      '%s<p><a href="%s">%s</a></p>',
      $postContent,
      $post->edit_url,
      '(Admin only) Edit post'
    ) 
  }
 
  echo $postContent;
}

Trong GraphQL, chúng ta có thể thực thi có điều kiện thao tác này hay thao tác khác, tạo ra các giá trị khác nhau cho một trường:

query InitializeDynamicVariables
{
  isAdminUser: _echo(value: false)
    @export(as: "isAdminUser")
}
 
query ExportConditionalVariables
  @depends(on: "InitializeDynamicVariables")
{
  me {
    roleNames
    isAdminUser: _inArray(
      value: "administrator",
      array: $__roleNames
    )
      @export(as: "isAdminUser")
  }
}
 
query RetrieveContentForAdminUser($postId: ID!)
  @depends(on: "ExportConditionalVariables")
  @include(if: $isAdminUser)
{
  post(by: { id : $postId }) {
    originalContent: content
    wpAdminEditURL
    content: _sprintf(
      string: "%s<p><a href=\"%s\">%s</a></p>",
      values: [
        $__originalContent,
        $__wpAdminEditURL,
        "(Admin only) Edit post"
      ]
    )
  }
}
 
query RetrieveContentForNonAdminUser($postId: ID!)
  @depends(on: "ExportConditionalVariables")
  @skip(if: $isAdminUser)
{
  post(by: { id : $postId }) {
    content
  }
}
 
query ExecuteAll
  @depends(on: [
    "RetrieveContentForAdminUser",
    "RetrieveContentForNonAdminUser"
  ])
{
  # ...
}

Bằng cách sử dụng các directive @include@skip với cùng một biến động làm đầu vào, các thao tác RetrieveContentForAdminUserRetrieveContentForNonAdminUser là loại trừ lẫn nhau.

Lặp qua mảng

Giả sử chúng ta muốn lặp qua các phần tử trong một mảng, và chuyển đổi các giá trị đó thành chữ hoa:

function printUserRolesAsUppercase(int $userID)
{
  $user = getUser($userID);
  foreach ($user->roles as $role) {
    echo strtoupper($role);
  }
}

Trong GraphQL, chúng ta có thể dùng directive @underEachArrayItem để lặp qua các phần tử mảng, và cung cấp từng giá trị đó cho directive tiếp theo trong chuỗi, trong trường hợp này là @strUpperCase:

query PrintUserRolesAsUppercase($userID: ID!)
{
  user(by: { id: $userID }) {
    roles
      @underEachArrayItem
        @strUpperCase
  }
}

Điều này có thể thực hiện được nhờ các directive có thể kết hợp.

Thao tác CRUD hàng loạt

CRUD là viết tắt của Create (Tạo), Read (Đọc), Update (Cập nhật) và Delete (Xóa), đây là các thao tác chúng ta áp dụng trên các tài nguyên (bài viết, người dùng, v.v.).

Đọc hàng loạt trong PHP trông như thế này:

function getPostTitles()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    echo $post->title;
  }
}

Trường hợp sử dụng này được GraphQL thỏa mãn một cách tự nhiên:

query GetPostTitles
{
  posts {
    title
  }
}

Cập nhật hàng loạt trong PHP trông như thế này:

function updatePostTitlesAsUppercase()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    $post->update(['title' => strtoupper($post->title)]);
  }
}

Thực thi cập nhật hàng loạt trong GraphQL thường được hỗ trợ bằng cách tạo một mutation chuyên dụng updatePosts, lấy dữ liệu cho tất cả các bài viết.

Tôi không thích cách tiếp cận này, vì nó thực sự nhân đôi số lượng mutation trong schema (một để thay đổi tài nguyên đơn lẻ, một để thay đổi nhiều tài nguyên), và chúng ta cần duy trì logic cho cả hai:

  • updatePost + updatePosts
  • createPost + createPosts
  • v.v.

Theo ý kiến của tôi, một cách tiếp cận thanh lịch hơn là sử dụng nested mutations, nơi mutation Post.update được áp dụng cho từng tài nguyên được truy vấn:

mutation UpdatePostTitlesAsUppercase
{
  posts {
    title
    ucTitle: _strUpperCase(text: $__title)
    update(
      input: { title: $__ucTitle }
    ) {
      status
      post {
        title
      }
    }
  }
}

Cách tiếp cận tương tự cũng hoạt động khi xóa tài nguyên:

function deletePosts()
{
  $posts = getPosts();
  foreach ($posts as $post) {
    $post->delete();
  }
}

Trong GraphQL:

mutation DeletePosts
{
  posts {
    delete {
      status
    }
  }
}

Đối với việc tạo, chúng ta không truyền các tài nguyên vì chúng chưa tồn tại; thay vào đó, chúng ta cung cấp một mảng với các dữ liệu đầu vào cho tất cả tài nguyên cần tạo:

function createPosts()
{
  $postDataItems = [
    [
      'title' => 'First title',
      'content' => 'First content',
    ],
    [
      'title' => 'Second title',
      'content' => 'Second content',
    ],
  ];
  foreach ($postDataItems as $postDataItem) {
    $post = new Post($postDataItem['title'], $postDataItem['content']);
    $post->save();
  }
}

Tạo bài viết hàng loạt trong GraphQL sử dụng một mutation createPost duy nhất hơi phức tạp, nhưng vẫn có thể thực hiện được.

Ý tưởng là lặp qua mảng với các dữ liệu đầu vào, gán từng phần tử dưới một biến động $input, và sau đó thực thi mutation createPost truyền vào đó. Cuối cùng chúng ta lấy các ID kết quả từ các bài viết đã tạo dưới biến động $createdPostIDs, và truy xuất dữ liệu của chúng:

mutation CreatePosts
  @depends(on: "GetPostsAndExportData")
{
  createdPostIDs: _echo(value: [
    {
      title: "First title",
      content: "First content"
    },
    {
      title: "Second title",
      content: "Second content"
    },
  ])
    @underEachArrayItem(
      passValueOnwardsAs: "input"
    )
      @applyField(
        name: "createPost"
        arguments: {
          input: $input
        },
        setResultInResponse: true
      )
    @export(as: "createdPostIDs")
}
 
query RetrieveCreatedPosts
  @depends(on: "CreatePosts")
{
  createdPosts: posts(
    filter: {
      ids: $createdPostIDs,
    }
  ) {
    title
    content
  }
}

Gửi một yêu cầu HTTP (và các hàm khác)

Gửi một yêu cầu HTTP đến một máy chủ web có thể được thực hiện qua một hàm chuyên dụng trong PHP, chẳng hạn file_get_contents hoặc curl_exec.

Sử dụng file_get_contents:

$xml = file_get_contents("http://www.example.com/file.xml");

Trong GraphQL, logic để thực thi một yêu cầu HTTP có thể được thực hiện qua một trường chức năng, chẳng hạn _sendHTTPRequest:

query {
  _sendHTTPRequest(input: {
    url: "http://www.example.com/file.xml",
    method: GET
  }) {
    xml: body
  }
}

Khái niệm tương tự áp dụng cho bất kỳ chức năng nào.

Ví dụ, chúng ta truy cập giá trị của một hằng số trong PHP như thế này:

$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');

Chúng ta có thể triển khai một trường chức năng tương ứng trong GraphQL:

{
  mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
}

Giải quyết bài toán chỉ bằng GraphQL

Với tất cả các tính năng ngôn ngữ lập trình chúng ta vừa khám phá, chúng ta hiện có thể sử dụng chỉ GraphQL để giải quyết vấn đề đã đặt ra trước đó:

  • Tạo một webhook để được gọi bởi một dịch vụ mỗi khi một người dùng mới đăng ký dịch vụ đó; người dùng có thể đã đăng ký nhận bản tin (được chỉ ra bởi trường marketing_optin trong payload của webhook); trong trường hợp đó, webhook phải đăng ký email của người dùng (trong trường email của payload webhook) vào một danh sách Mailchimp.

Giải pháp là sử dụng một GraphQL persisted query làm webhook, với query sau:

query HasSubscribedToNewsletter {
  hasSubscriberOptIn: _httpRequestHasParam(name: "marketing_optin")
  subscriberOptIn: _httpRequestStringParam(name: "marketing_optin")
  isNotSubscriberOptInNAValue: _notEquals(value1: $__subscriberOptIn, value2: "NA")
  subscribedToNewsletter: _and(values: [$__hasSubscriberOptIn, $__isNotSubscriberOptInNAValue])
    @export(as: "subscribedToNewsletter")
}
 
query MaybeCreateContactOnMailchimp
   @depends(on: "HasSubscribedToNewsletter")
   @include(if: $subscribedToNewsletter)
{
  subscriberEmail: _httpRequestStringParam(name: "email")
  
  mailchimpUsername: _env(name: "MAILCHIMP_API_CREDENTIALS_USERNAME")
   
  mailchimpPassword: _env(name: "MAILCHIMP_API_CREDENTIALS_PASSWORD")
   
  
  mailchimpListMembersJSONObject: _sendJSONObjectItemHTTPRequest(input: {
    url: "https://us7.api.mailchimp.com/3.0/lists/{listCode}/members",
    method: POST,
    options: {
      auth: {
        username: $__mailchimpUsername,
        password: $__mailchimpPassword
      },
      json: {
        email_address: $__subscriberEmail,
        status: "subscribed"
      }
    }
  })
}

Trong giải pháp này, thao tác MaybeCreateContactOnMailchimp, thực thi yêu cầu HTTP đến API của Mailchimp, sẽ được thực thi có điều kiện, tùy thuộc vào giá trị của trường marketing_optin.

(Đọc bài viết blog 👨🏻‍🏫 GraphQL query to automatically send the newsletter subscribers from InstaWP to Mailchimp để xem cách query này hoạt động.)

GraphQL mạnh mẽ hơn bạn nghĩ!

GraphQL có thể được sử dụng cho nhiều thứ hơn là chỉ lấy và thay đổi dữ liệu... Điều chỉnh dữ liệu, sửa đổi đầu ra một cách động, tùy chỉnh nội dung cho các ngữ cảnh khác nhau, tạo một API gateway chỉ với vài dòng code, và nhiều thứ khác.

Bằng cách hỗ trợ các tính năng ngôn ngữ lập trình, chúng ta có thể giải quyết bài toán trên chỉ bằng GraphQL, và tránh phải triển khai một client đi kèm với nó. Chúng ta đang đơn giản hóa ngăn xếp ứng dụng: Ít bộ phận chuyển động hơn, ít phức tạp hơn, ít code cần debug hơn, ít công nghệ cần xử lý hơn.

GraphQL thật tuyệt vời 🤘


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