ブログ

👨🏻‍💻 GraphQL(一種の)プログラミング言語として

Leonardo Losoviz
著者: Leonardo Losoviz ·

GraphQLはGraphQL言語を持っていますが、プログラミング言語と通常は呼ばれません。プログラミング言語でできることの多くがGraphQLではできないからです。

GraphQLは通常、データの取得(例えばクライアント側でウェブサイトをレンダリングする場合)やデータの変更(例えば投稿を作成する場合)に使われます。それが基本的な使い方です。

(その他の用途は、この2つのケースの組み合わせに過ぎません。例えば、APIゲートウェイはクライアントに公開されていない内部サーバーからデータを取得・変更することがあります。)

GraphQLでデータにアクセスする場合:

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

...これはPHPでは次のように(ほぼ)同等です:

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

(以下のすべての例では、比較のプログラミング言語としてPHPを使用します。)

GraphQLでデータを変更する場合:

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

...これはPHPでは次のように(ほぼ)同等です:

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

これで十分な理由は、GraphQLは通常クライアント(JavaScript、PHP、Javaなどのプログラミング言語で書かれた)からアクセスされ、クライアントがデータの処理ロジックを持つからです。つまり、GraphQLは単独で使われるのではなく、他の何かと組み合わせて使われます。

しかし、GraphQLが単独で使えるようになれば、GraphQLだけで多くの新しいユースケースを解決でき、GraphQLを新しい環境にデプロイして、アプリケーションスタックの追加タスクを担当させることができます。

そのためには、GraphQLがプログラミング言語の多くの機能をサポートする必要があります。

GraphQLがサポートするプログラミング言語の機能は限られています。例えば、ディレクティブ@include(または@skip)を使って変数を入力として渡すことは、(一種の)条件ロジックと見なせます:

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

このクエリはPHPでは次のように同等です:

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

それが基本的なところです。GraphQLには再帰、動的変数(値が実行時に計算されて変数に代入されるもので、ディクショナリへの入力としてではなく)、変数代入(例:フィールドの出力を変数に代入し、それを別のフィールドの引数として渡す)などが欠けています。

次の問題を、GraphQLだけを使ってどのように解決するか考えてみてください:

  • あるサービスに新しいユーザーが登録するたびにそのサービスから呼び出されるWebhookを作成する。ユーザーはニュースレターを購読している場合があり(webhookのペイロードのmarketing_optinフィールドで示される)、その場合、webhookはユーザーのメールアドレス(webhookのペイロードのemailフィールド)をMailchimpのリストに登録しなければならない。

これは実現可能だと思いますか?簡単?難しい?不可能?

Gato GraphQLでは、この問題をGraphQLだけで解決したいと考えています。そして、さらに多くの問題も。そのために、プログラミング言語の特性をサポートする方法について深く考えてきました。

GraphQLサーバーでサポートしたプログラミング機能について見ていきましょう。この記事の最後で、その問題をどのように解決できるかを見ていきます。

機能性(Functionality)

GraphQLのフィールドは通常、投稿のタイトル、コンテンツ、データなどのデータを取得します。しかし、フィールドを「機能性」として実装することもできます。

例えば、PHPで時刻を出力する場合:

function printTime()
{
  echo time();
}

...これはGraphQLのフィールド_timeで実現できます:

{
  _time
}

関数timeはどの型にも属さないため、フィールド_timeも同様です。そのため、これはグローバルフィールドであり、GraphQLスキーマのすべての型からアクセスできます:

{
  posts {
    _time
  }
}

その他の機能性フィールドの例:

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

関数(Functions)

ロジックの単位を関数に分割し、ある関数が別の関数を呼び出すようにすることができます:

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

GraphQLでも同様に、ドキュメント内のquery(またはmutation)操作を複数のquery操作に分割し、ある操作が他の操作に「依存する」ようにして、それらを先に実行させることができます:

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"
  ])
{
  # ...
}

このクエリでは、エンドポイントに?operationName=PrintPostPropertiesを渡してGraphQLクエリを実行すると、まずPrintPostTitlePrintPostContentのクエリが実行され、その後にPrintPostPropertiesが実行されます。

これはMultiple Query Executionによって可能になります。

動的変数(Dynamic Variables)

値を実行時に計算して変数に代入できます。そして、その値に基づいて、ある機能を条件付きで実行するかどうかを決定できます:

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

GraphQLでは、ある操作で動的変数に値を「エクスポート」し、別の操作でその値を読み取ることができます:

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

実行時に計算された値を保持する変数$addContentは、PrintPostProperties操作で読み取られますが、その操作では宣言されていないことに注意してください。これは動的変数だからです。

関数の条件付き実行

前の例の代替として、ロジックを関数にグループ化し、動的変数の値に応じて関数を条件付きで実行するかどうかを決定することができます:

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

GraphQLでは、操作に@includeディレクティブを追加できます:

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"
  ])
{
  # ...
}

これで、$addContenttrueの場合にのみPrintPostContent操作が実行されます。

変数の代入と再入力

前の例を少し修正してみましょう。前の例では、条件"addContent"はユーザーがログインしているかどうかに結びついていました。

この別の例では、"addContent"は今日が週末である場合にtrueとなります。これには計算のためのロジックが必要です:

  • 今日の日付を取得する
  • 日付を曜日名(小文字)にフォーマットする
  • それが"saturday"または"sunday"であるかを確認する

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

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

ExportAddContent操作では、クエリされた各フィールドの値が、動的変数$__fieldNameとして下のフィールドで即座に利用可能になります。これにより、フィールドの出力を同じ操作内で即座に別のフィールドの入力として使用できます。

これはField to Inputによって可能になります。

値の動的な変更

このPHPの例では、ログインユーザーが管理者である場合に変数の値を変更し、その場合に投稿のコンテンツに投稿を編集するリンクを追加します:

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

GraphQLでは、ある操作または別の操作を条件付きで実行し、特定のフィールドに異なる値を生成することができます:

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"
  ])
{
  # ...
}

同じ動的変数を入力として@include@skipディレクティブを使うことで、RetrieveContentForAdminUserRetrieveContentForNonAdminUserの操作は相互に排他的になります。

配列の反復処理

配列のアイテムを反復処理して、それらの値を大文字に変換したいとします:

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

GraphQLでは、ディレクティブ@underEachArrayItemで配列アイテムを反復処理し、それぞれの値をチェーン内の次のディレクティブ(この場合は@strUpperCase)に渡すことができます:

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

これはコンポーザブルディレクティブによって可能になります。

一括CRUD操作

CRUDはCreate(作成)、Read(読み取り)、Update(更新)、Delete(削除)の略で、リソース(投稿、ユーザーなど)に対して行う操作です。

PHPでの一括読み取りはこのようになります:

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

このユースケースはGraphQLで自然に満たされます:

query GetPostTitles
{
  posts {
    title
  }
}

PHPでの一括更新はこのようになります:

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

GraphQLでの一括更新は、通常、すべての投稿のデータを受け取る専用のmutation updatePostsを作成することでサポートされています。

私はこのアプローチが好きではありません。スキーマ内のmutationの数を事実上2倍にし(1つのリソースを変更するもの、複数のリソースを変更するもの)、両方のロジックを維持する必要があるからです:

  • updatePost + updatePosts
  • createPost + createPosts
  • など

私の考えでは、より優れたアプローチはネストされたmutationを使うことです。そこでは、mutation Post.updateがクエリされた各リソースに適用されます:

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

同じアプローチがリソースの削除にも機能します:

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

GraphQLの場合:

mutation DeletePosts
{
  posts {
    delete {
      status
    }
  }
}

作成の場合は、リソースがまだ存在しないため渡しません。代わりに、作成するすべてのリソースのデータ入力を含む配列を提供します:

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();
  }
}

単一のcreatePost mutationを使ってGraphQLで一括投稿を作成するのは少し複雑ですが、それでも実現可能です。

考え方は、データ入力の配列を反復処理し、それぞれを動的変数$inputに代入し、その入力を渡してcreatePost mutationを実行することです。最後に、動的変数$createdPostIDsの下に作成された投稿の結果IDを取得し、そのデータを取得します:

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

HTTPリクエストの送信(およびその他の関数)

ウェブサーバーへのHTTPリクエストの送信は、PHPのfile_get_contentscurl_execなどの専用関数で行えます。

file_get_contentsを使う場合:

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

GraphQLでは、HTTPリクエストを実行するロジックは_sendHTTPRequestなどの機能性フィールドで満たすことができます:

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

同じ考え方がどんな機能にも適用されます。

例えば、PHPでは定数の値に次のようにアクセスします:

$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');

GraphQLで対応する機能性フィールドを実装できます:

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

GraphQLだけを使って課題を解決する

これまで取り上げたすべてのプログラミング言語の機能を使って、先ほど提起した問題をGraphQLだけで解決できるようになりました:

  • あるサービスに新しいユーザーが登録するたびにそのサービスから呼び出されるWebhookを作成する。ユーザーはニュースレターを購読している場合があり(webhookのペイロードのmarketing_optinフィールドで示される)、その場合、webhookはユーザーのメールアドレス(webhookのペイロードのemailフィールド)をMailchimpのリストに登録しなければならない。

解決策は、GraphQLのパーシステッドクエリをWebhookとして使用し、次のクエリを実行することです:

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"
      }
    }
  })
}

この解決策では、MailchimpのAPIに対してHTTPリクエストを実行するMaybeCreateContactOnMailchimp操作が、marketing_optinフィールドの値に応じて条件付きで実行されます。

(このクエリの動作については、ブログ記事👨🏻‍🏫 GraphQL query to automatically send the newsletter subscribers from InstaWP to Mailchimpをご覧ください。)

GraphQLはあなたが思っていたより強力です!

GraphQLはデータの取得と変更だけでなく、さらに多くのことに使えます...データの適応、出力の動的な変更、異なるコンテキストへのコンテンツのカスタマイズ、わずか数行のコードでのAPIゲートウェイの作成など、さまざまな用途があります。

プログラミング言語の機能をサポートすることで、上記の課題をGraphQLだけで解決し、一緒にデプロイするクライアントを不要にすることができます。これにより、アプリケーションスタックを簡素化できます:可動部品が少なく、複雑さが減り、デバッグすべきコードが減り、対処すべき技術が少なくなります。

GraphQL、最高です 🤘


ニュースレターを購読する

Gato GraphQL のすべてのアップデートを把握しましょう。