Blog

👨🏻‍💻 GraphQL (bir tür) programlama dili olarak

Leonardo Losoviz
Yazan: Leonardo Losoviz ·

GraphQL, GraphQL diline sahip olmasına rağmen, normalde bir programlama dili olarak adlandırılmaz; çünkü programlama dilleriyle yapabildiğimiz pek çok şeyi GraphQL ile yapamayız.

GraphQL normalde veri çekmek için kullanılır; örneğin istemcide bir web sitesini render etmek için, ve veriyi değiştirmek için; örneğin bir gönderi oluşturmak için. Hepsi bu kadar.

(Diğer kullanımlar yalnızca bu 2 önceki durumun kombinasyonlarıdır. Örneğin, bir API gateway istemciye açık olmayan bir iç sunucudan veri çekebilir/değiştirebilir.)

GraphQL'de veriye erişim:

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

...PHP'de şu (türden bir) karşılığa sahiptir:

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

(Aşağıdaki tüm örnekler karşılaştırma için programlama dili olarak PHP kullanacaktır.)

GraphQL'de veri mutasyonu:

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

...PHP'de şu (türden bir) karşılığa sahiptir:

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

Bu yeterlidir; çünkü GraphQL normalde bir istemciden erişilir (JavaScript, PHP, Java veya başka bir programlama diliyle yazılmış), ve bu istemci verilerle ne yapılacağının mantığını içerir. Dolayısıyla GraphQL tek başına kullanılmaz; başka birinin yardımcısı olarak çalışır.

Ancak GraphQL tek başına kullanılabilseydi, yalnızca GraphQL kullanılarak pek çok yeni kullanım senaryosu çözülebilirdi; böylece GraphQL yeni ortamlara dağıtılabilir ve uygulama katmanında ek görevler üstlenebilirdi.

Bunun için GraphQL'in programlama dillerinin özelliklerinin çoğunu desteklemesi gerekir.

GraphQL'in desteklediği programlama dili özellikleri sınırlıdır. Örneğin, @include (veya @skip) direktifini kullanmak ve bir değişkeni girdi olarak iletmek (bir tür) koşullu mantık olarak değerlendirilebilir:

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

Bu query'nin PHP karşılığı şudur:

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

Hepsi bu kadar. GraphQL'de özyineleme, dinamik değişkenler (değerleri çalışma zamanında hesaplanıp değişkene atanan, girdi sözlüğünde değil), değişken atamaları (örn: bir alanın çıktısını bir değişkene atamak, bu değişkenin daha sonra başka bir alana argüman olarak sağlanması) ve benzeri özellikler eksiktir.

Aşağıdaki problemi yalnızca GraphQL kullanarak nasıl çözerdiniz bir düşünün:

  • Bir servise yeni bir kullanıcı kaydolduğunda servis tarafından çağrılacak bir webhook oluşturun; kullanıcı bültene abone olmuş olabilir (webhook yükündeki marketing_optin alanıyla belirtilir); bu durumda webhook, kullanıcının e-posta adresini (webhook yükündeki email alanında) bir Mailchimp listesine kaydetmelidir.

Bunu yapılabilir mi buluyorsunuz? Kolay mı? Zor mu? İmkansız mı?

Gato GraphQL'de bu problemi yalnızca GraphQL kullanarak çözmek istiyoruz. Ve çok daha fazla problemi. Bu yüzden programlama dillerinin özelliklerini nasıl destekleyeceğimiz üzerine uzun uzun düşündük.

GraphQL sunucumuzda desteklediğimiz programlama özelliklerini keşfedelim. Bu yazının sonunda, söz konusu problemi nasıl çözebileceğimizi göreceğiz.

İşlevsellik

GraphQL'deki alanlar normalde bir gönderinin başlığı, içeriği veya verisi gibi bilgileri getirir. Ancak alanları "işlevsellik" olarak da uygulayabiliriz.

Örneğin, PHP'de zamanı yazdırmak:

function printTime()
{
  echo time();
}

...GraphQL'de _time alanıyla yapılabilir:

{
  _time
}

time fonksiyonunun hiçbir türe ait olmadığına dikkat edin; dolayısıyla _time alanı da öyle değildir. Bu nedenle bu bir global alandir ve GraphQL şemasındaki her türden erişilebilir:

{
  posts {
    _time
  }
}

Diğer işlevsellik alanı örnekleri:

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

Fonksiyonlar

Mantık birimlerini fonksiyonlara bölebilir ve bir fonksiyonun başka bir fonksiyonu çağırmasını sağlayabiliriz:

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

GraphQL'de benzer şekilde belgedeki query (veya mutation) işlemini birden fazla query işlemine bölebilir ve bir işlemin diğerlerine "bağımlı" olmasını sağlayarak önce onları çalıştırabiliriz:

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

Bu query'de, GraphQL sorgusunu endpoint'e ?operationName=PrintPostProperties geçirerek çalıştırmak önce PrintPostTitle ve PrintPostContent queries'ini, ardından PrintPostProperties'i çalıştırır.

Bu, Çoklu Query Çalıştırma sayesinde mümkündür.

Dinamik Değişkenler

Çalışma zamanında bir değer hesaplayıp değişkene atayabiliriz. Ardından bu değere göre koşullu olarak bir işlevselliği çalıştırıp çalıştırmayacağımıza karar verebiliriz:

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

GraphQL'de bir işlemde dinamik bir değişken olarak bir değeri "dışa aktarabilir" ve ardından başka bir işlemde bu değeri okuyabiliriz:

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

Çalışma zamanında hesaplanmış bir değer tutan $addContent değişkeninin PrintPostProperties işleminde okunduğuna ancak tanımlanmadığına dikkat edin; çünkü bu bir dinamik değişkendir.

Fonksiyonları Koşullu Olarak Çalıştırma

Önceki örneğe alternatif olarak, mantığı fonksiyonlara gruplandırabilir ve dinamik değişkenin değerine bağlı olarak bir fonksiyonu koşullu olarak çalıştırıp çalıştırmayabiliriz:

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'de işleme @include direktifini ekleyebiliriz:

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

Artık PrintPostContent işlemi yalnızca $addContent true olduğunda çalıştırılacaktır.

Değişken Atama, Girdi Olarak Geri Besleme

Önceki örneği biraz değiştirelim; bu örnekte "addContent" koşulu kullanıcının giriş yapıp yapmadığına bağlıydı.

Bu diğer örnekte ise "addContent" bugünün hafta sonu olması durumunda true'dur; bu da hesaplanması için biraz mantık gerektirir:

  • Bugünün tarihini al
  • Günün adını küçük harfle biçimlendir
  • "saturday" veya "sunday" olup olmadığını kontrol et

PHP'de:

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'de:

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 işleminde, sorgulanan her alanın değeri dinamik değişken $__fieldName altında hemen aşağıdaki alanlara sunulur. Bu sayede bir alanın çıktısı, aynı işlem içinde bile hemen başka bir alana girdi olarak kullanılabilir.

Bu, Field to Input sayesinde mümkündür.

Bir Değeri Dinamik Olarak Değiştirme

Bu PHP örneğinde, giriş yapmış kullanıcı bir yönetici olduğunda bir değişkenin değerini değiştiriyoruz; bu durumda gönderinin içeriğine gönderiyi düzenleme bağlantısı ekleniyor:

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'de koşullu olarak bir işlem veya başka bir işlem çalıştırabilir, böylece belirli bir alan için farklı değerler üretebiliriz:

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 ve @skip direktiflerini aynı dinamik değişkenle kullanarak RetrieveContentForAdminUser ve RetrieveContentForNonAdminUser işlemleri birbirini dışlayan hale gelir.

Diziler Üzerinde Yineleme

Bir dizideki öğeleri yinelemek ve bu değerleri büyük harfe dönüştürmek istediğimizi varsayalım:

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

GraphQL'de @underEachArrayItem direktifinin dizi öğeleri üzerinde yineleme yapmasını ve bu değerlerin her birini zincirdeki bir sonraki direktife, bu durumda @strUpperCase'e iletmesini sağlayabiliriz:

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

Bu, birleştirilebilir direktifler sayesinde mümkündür.

Toplu CRUD İşlemleri

CRUD, Oluşturma (Create), Okuma (Read), Güncelleme (Update) ve Silme (Delete) anlamına gelir; bunlar kaynaklar (gönderiler, kullanıcılar vb.) üzerinde uyguladığımız işlemlerdir.

PHP'de toplu okuma şöyle görünür:

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

Bu kullanım senaryosu doğal olarak GraphQL tarafından karşılanır:

query GetPostTitles
{
  posts {
    title
  }
}

PHP'de toplu güncelleme şöyle görünür:

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

GraphQL'de toplu güncellemeler normalde tüm gönderilerin verilerini alan özel bir updatePosts mutation'ı oluşturularak desteklenir.

Bu yaklaşımı beğenmiyorum; çünkü şemadaki mutation sayısını fiilen ikiye katlar (tek kaynağı değiştirmek için bir, birden fazla kaynağı değiştirmek için bir) ve her ikisinin mantığını sürdürmemiz gerekir:

  • updatePost + updatePosts
  • createPost + createPosts
  • vb.

Bence daha zarif bir yaklaşım, Post.update mutation'ının sorgulanan kaynakların her birine uygulandığı iç içe mutation'lar kullanmaktır:

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

Kaynakları silmek için de aynı yaklaşım geçerlidir:

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

GraphQL'de:

mutation DeletePosts
{
  posts {
    delete {
      status
    }
  }
}

Oluşturma için, kaynaklar henüz mevcut olmadığından onları geçmeyiz; bunun yerine oluşturulacak tüm kaynakların girdi verileriyle bir dizi sağlarız:

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

GraphQL'de tek bir createPost mutation'ı kullanarak toplu gönderi oluşturmak biraz karmaşıktır; ancak yine de mümkündür.

Fikir, girdi verileriyle diziyi yinelemek, her birini $input dinamik değişkeni altında atamak ve ardından bu girdiyi ileterek createPost mutation'ını çalıştırmaktır. Son olarak oluşturulan gönderilerin ID'lerini $createdPostIDs dinamik değişkeni altında alır ve verilerini çekeriz:

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 İsteği Gönderme (ve Diğer Fonksiyonlar)

Bir web sunucusuna HTTP isteği göndermek, PHP'de file_get_contents veya curl_exec gibi özel bir fonksiyon aracılığıyla gerçekleştirilebilir.

file_get_contents kullanımı:

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

GraphQL'de, HTTP isteği çalıştırma mantığı _sendHTTPRequest gibi bir işlevsellik alanı aracılığıyla sağlanabilir:

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

Aynı kavram tüm işlevsellikler için geçerlidir.

Örneğin, PHP'de bir sabitin değerine şu şekilde erişiriz:

$mailchimpUsername = constant('MAILCHIMP_API_CREDENTIALS_USERNAME');

GraphQL'de karşılık gelen bir işlevsellik alanı uygulayabiliriz:

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

Yalnızca GraphQL Kullanarak Zorluğu Çözme

Az önce ele aldığımız tüm programlama dili özellikleriyle, daha önce ortaya konulan problemi yalnızca GraphQL kullanarak çözebilecek konuma geldik:

  • Bir servise yeni bir kullanıcı kaydolduğunda servis tarafından çağrılacak bir webhook oluşturun; kullanıcı bültene abone olmuş olabilir (webhook yükündeki marketing_optin alanıyla belirtilir); bu durumda webhook, kullanıcının e-posta adresini (webhook yükündeki email alanında) bir Mailchimp listesine kaydetmelidir.

Çözüm, bu query ile bir webhook olarak bir GraphQL persisted query kullanmaktır:

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

Bu çözümde, Mailchimp API'sine karşı HTTP isteğini çalıştıran MaybeCreateContactOnMailchimp işlemi, marketing_optin alanının değerine bağlı olarak koşullu biçimde çalıştırılacaktır.

(Bu query'nin nasıl çalıştığını görmek için 👨🏻‍🏫 InstaWP'den Mailchimp'e bülten abonelerini otomatik olarak göndermek için GraphQL query blog yazısını okuyun.)

GraphQL Düşündüğünüzden Çok Daha Güçlü!

GraphQL, yalnızca veri çekmek ve değiştirmekten çok daha fazlası için kullanılabilir... Veriyi uyarlamak, çıktıyı dinamik olarak değiştirmek, farklı bağlamlar için içeriği özelleştirmek, yalnızca birkaç satır kodla bir API gateway oluşturmak ve çok daha fazlası.

Programlama dili özelliklerini destekleyerek yukarıdaki zorluğu yalnızca GraphQL kullanarak çözebilir ve yanında konuşlandırılacak bir istemciden kaçınabiliriz. Böylece uygulama katmanını sadeleştiriyoruz: Daha az hareketli parça, daha az karmaşıklık, daha az hata ayıklanacak kod, daha az teknoloji ile uğraşmak.

GraphQL harika 🤘


Bültenimize abone olun

Gato GraphQL'deki tüm güncellemelerden haberdar olun.