Blog

🕸 GraphQL WordPress'i Nasıl ve Nerede İyileştirebilir, REST API'yi Tamamlayarak

Leonardo Losoviz
Yazan: Leonardo Losoviz ·

Güncelleme 01/05/2024: Gato GraphQL vs WP REST API karşılaştırmasına göz atın.

Geçen hafta sonu 🦸🏿‍♂️ Gato GraphQL artık PHP 8.0'dan 7.1'e transpile ediliyor başlıklı blog yazımı yayınladım.

Yazıyı Reddit'teki /r/php'de paylaştıktan sonra, topluluk GraphQL'in WordPress'te kullanılmasının ne kadar değerli olduğu, WP REST API'den ne kadar farklı olduğu ve WordPress'e bir API daha eklemenin ne kadar haklı olduğu konularında canlı bir tartışma başlattı.

Bence yorumların çoğu yerinde, bazıları ise önemli bilgilerden yoksun. GraphQL yalnızca bir arayüz değil, aynı zamanda bir uygulamadır. Bu, farklı sağlayıcılardan gelen farklı GraphQL sunucularının farklı özelliklere öncelik verecek şekilde tasarlanmış olabileceği anlamına gelir. Bu nedenle, GraphQL'in neler sunduğuna dair her zaman birleşik bir beklentiye veya bir GraphQL motorunun nasıl çalıştığına dair tam bir anlayışa sahip olamayız.

Örneğin, WordPress'teki ve Laravel'deki GraphQL deneyimi farklı olacaktır; farklı sunucular olan WPGraphQL ve Gato GraphQL'in sağladığı deneyim de farklıdır.

Bu makale, Reddit gönderisindeki çeşitli yorumlara yanıt veren kişisel görüşümü içermektedir.

GraphQL vs WP REST API

[Bu gerçekten kötü bir fikir,] zaten kendi REST API'sini kullanan WordPress'in üzerine bir GraphQL API eklemek. Sadece REST API kullanın. [Kaynak]

REST API ve GraphQL aynı amaca hizmet eder: uygulamaya ihtiyaç duyduğu verileri sağlamak. Ancak bunu nasıl gerçekleştirdikleri konusunda farklı davranırlar: REST belirli bir veri kümesi sağlayan önceden tanımlanmış endpoint'lere sahipken, GraphQL tam olarak ihtiyaç duyulan verileri sağlayabilir.

Bu farklı davranış, uygulamanın performansı üzerinde doğrudan bir etkiye sahip olabilir. REST ile, gönderi listesi ve her gönderi yazarının bazı verilerini almamız gerektiğinde, ek istekler göndermemiz gerekecektir. Muhtemelen tüm yazar verileri için 1 ek istek veya yazar başına 1 ek istek. Bu süre zarfında, web sitesinin ziyaretçisi sayfanın render edilmesini bekliyor olabilir.

GraphQL bu durumu iyileştirir, çünkü tüm gönderi ve yazar verilerini tek bir istekte doğrudan çekebiliriz ve web sayfasının render edilmesi daha hızlı olacaktır:

{
  posts {
    id
    title
    excerpt
    date
    url
    author {
      id
      name
      url
    }
  }
}

Dolayısıyla, WordPress'te zaten REST API mevcut olsa bile, bu her görev için her zaman en uygun araç olduğu anlamına gelmez. Elbette her zaman kullanabiliriz, ancak GraphQL'e de erişimimiz varsa, bu API'yi REST'e göre avantaj sağladığı durumlarda kullanmayı tercih edebiliriz ve bu daha iyi bir sonuç verir.

GraphQL için Zor Başlangıç Kurulumu + Resolver Yazmak Zorunda Kalmak

GraphQL için başlangıç kurulumunun REST'e kıyasla katlanarak daha yüksek olduğuna dair kesinlikle bir argüman öne sürülebilir; ilişkilendirmelerin ayarlanması gerektiği konusunda haklısınız. [Kaynak]

Ve...

Sizin ve web'deki neredeyse herkesin atladığı şey, bu API formatının çalışması için, REST ile mevcut olmayan bir dizi sorunu beraberinde getiren parser'ı (resolver'lar + tipler) yazmanız gerektiğidir. [Kaynak]

Bu yorumlar tamamen doğru değil, çünkü hem WPGraphQL hem de Gato GraphQL WordPress veri modelini zaten GraphQL şemasına eşlemiştir (WPGraphQL tam olarak, benim plugin'im büyük ölçüde).

Bu plugin'lerden herhangi birini yükledikten sonra, herhangi bir resolver oluşturmak veya varlıklar arasında ilişkilendirmeler kurmak zorunda kalmadan hemen uygulamanız için veri çekmeye başlayabilirsiniz.

Uygulamanın kendi varlıklarından özel veri çekmek için (CPT'ler gibi), bunların resolver'lar aracılığıyla eşlenmesi gerektiği ve bunu yapmanız gerekeceği doğrudur. Ancak bu REST'ten farklı değildir: CPT'nizden özel veri almanız gerekiyorsa, o özel verileri çekmek için bir REST endpoint'i oluşturmanız gerekecektir. Özel bir endpoint de bir resolver'dır.

Dolayısıyla, resolver ihtiyacı açısından REST ve GraphQL API hemen hemen aynıdır.

Şimdi, web sitelerine ve belgelere göz attığımızda, GraphQL'in kurulumu için daha fazla çaba gerektiği izlenimini vermektedir. Bu önyargıda bir gerçeklik payı var.

Bunun birkaç nedeni olduğunu düşünüyorum. Her şeyden önce, GraphQL (en az) iki bölümden oluşur:

  1. ne olduğu ve nasıl çalıştığı kavramı
  2. bazı gerçek uygulamalar sağlayan sunucular

graphql.org gibi resmi site dahil GraphQL belgelerine göz atarken, GraphQL'in arkasındaki kavramlara odaklanıldığını, resolver'ların ne olduğunu ve neden gerekli olduklarını ayrıntılı biçimde ele aldığını görürüz.

Bu, Laravel ve Lighthouse kullanmak gibi sıfırdan bir uygulama geliştirirken faydalıdır. Bu durumda, resolver'larınızı kodlamanız gerekir (ancak REST endpoint'lerinizi de oluşturmanız gerekirdi).

Öte yandan, WordPress zaten uygulamanın kendisidir ve WPGraphQL ile Gato GraphQL birer çözümdür. Bu iki plugin resolver'ları bizim için zaten oluşturmuştur, bu yüzden onlarla ilgilenmemize gerek yoktur (WP REST API'nin de başlangıçta bir endpoint seti sağlaması gibi, onlarla da ilgilenmemize gerek yok).

Ayrıca GraphQL daha geliştirici odaklıdır ve belgeleri doğrudan geliştiricilere hitap ediyor gibi görünmektedir. Geliştiriciler sunucu tarafında resolver'lar oluşturur ve geliştiriciler istemci tarafında özel queries ile bu resolver'ları kullanır. Resolver oluşturmak geliştiricilere yönelik bir görev olduğundan, doğal ve sık biçimde gün yüzüne çıkmaktadır.

REST için beklenti (bence), gerekli verileri sağlayan endpoint'in zaten var olmasıdır (WP REST API tarafından sağlandığı gibi). Yoksa, ancak o zaman özel bir endpoint kurmakla ilgilenmemiz gerekir. Dolayısıyla, REST için resolver oluşturma üzerinde daha az vurgu yapılmaktadır.

Yani hem REST hem de GraphQL gerekli verileri sağlar. Ancak REST, endpoint'lerin zaten var olması gerektiği ve yalnızca var olmadığında onlarla ilgilendiğimiz statik bir yaklaşımı teşvik ederken, GraphQL her queries'in özel olarak yapıldığı ve ardından onun için mükemmel resolver'ı kodlayabildiğimiz dinamik bir yaklaşımı teşvik eder.

Sonuç olarak, REST ve GraphQL arasında temel farklılıklar yoktur, yalnızca gereksinimlerini nasıl karşılamaları gerektiği konusunda farklı yorumlar vardır.

GraphQL'de Güvenlik Açıkları + Güvenlik Değerlendirmeleri

Güvenli yorumlayıcı yazmak gerçekten zor olduğu için, bir gün GraphQL'den kaynaklanan büyük bir güvenlik açığı göreceğiz. [Kaynak]

Ve...

WordPress zaten o kadar büyük ki zaten büyük bir hedef haline gelmiş; HERHANGİ bir plugin eklemek çok fazla risk getiriyor ve güvenlik modelini atlamak için çok sayıda kod örneği içeren, adeta tüm WordPress'i açığa çıkaran bir plugin benim için kesinlikle hayır. Tema dışı çıktı, kesinlikle gerekli olanın ötesinde (ben istemediğim sürece mevcut olmayan) mümkün olduğunca kısıtlı olmalıdır. Bunun hiçbir zaman core'a girmemesini umuyorum. [Kaynak]

GraphQL, ele almamız gereken ek güvenlik riskleri getirir. Bu endişeye tamamen katılıyorum.

Ancak bunun, GraphQL'in WP core'a dahil edilmesini engelleyecek kadar büyük bir engel teşkil ettiğini düşünmüyorum. Üstelik, çözülmesinin gerçekten zor olduğunu da düşünmüyorum.

Gereken şey, GraphQL sunucusunun WordPress'in mevcut güvenlik mekanizmalarını kullanması ve ardından geliştiricinin bu mekanizmaları kullanarak belirli bir alana yalnızca uygun kullanıcıların erişebildiğinden emin olmasıdır:

  • kullanıcı giriş yapmış mı?
  • kullanıcı yönetici mi?
  • kullanıcının belirli bir rolü veya yetkisi var mı?
  • kullanıcı gönderinin yazarı mı?

Bu öneriyi karşılamak için Gato GraphQL, Erişim Kontrol Listeleri sunar; böylece her alan ve direktife kimin erişebileceğini yapılandırma yoluyla tanımlayabiliriz.

Bazen yalnızca bir ACL kullanmak yeterli olmaz ve GraphQL sunucusunun ekstra güvenlik önlemleri sağlaması gerekir. Gato GraphQL'in yaklaşan v0.8 sürümü için şu an üzerinde çalıştıklarımı anlatacağım.

posts alanı (gönderi verilerini almak için) yetkilendirme gerektirmez, giriş yapılmış olsun ya da olmasın herhangi bir kullanıcı ona erişebilir. Dolayısıyla güvenlik nedeniyle yalnızca yayımlanmış gönderileri çeker.

Ancak taslak/beklemede/çöpe atılmış gönderileri de almamız gereken durumlar vardır, örneğin:

  • Sitenin tüm verilerine erişimi olan yönetici tarafından çalıştırılan statik bir web sitesi oluşturmak için
  • Gönderi yazarları için, düzenlemeye devam edebilmek amacıyla tüm taslak gönderileri listelemek

Ardından aşağıdaki düzeni geliştirdim. Gönderileri çekmek için 3 alan olacak:

  • posts: herkese açık, yalnızca yayımlanmış gönderileri çekebilir
  • myPosts: herkese açık, yalnızca giriş yapmış kullanıcının gönderilerini herhangi bir durumla (yayımlanmış/taslak/beklemede/çöpe atılmış) çeker
  • postsForAdmin: yalnızca yönetici erişebilir, herhangi bir durumdaki herhangi bir gönderiyi çeker

Ve postsForAdmin varsayılan olarak devre dışıdır, bu nedenle yönetici açıkça etkinleştirmedikçe GraphQL şemasında bile görünmez (ve büyük olasılıkla yalnızca statik siteler oluşturmak için etkinleştirilecektir).

Bir diğer durum, bir alanın hem genel hem de özel verileri alabileceği durumdur. Örneğin, option alanı wp_options tablosundan veri alır. Bazı girdiler genel (blogname gibi), bazıları ise değildir (admin_email gibi).

Benzer bir durum, Post.metaValue, User.metaValue ve diğerleri aracılığıyla meta değerleri almak içindir. Örneğin, kullanıcı meta verileri kesinlikle özel olan wp_capabilities girişini içerirken, description geneldir. Sonra uygulamaya bağlı olarak genel veya özel olabilen last_name vardır.

Bu verilere erişimi güvenli kılmak için plugin, ayarlar sayfasındaki bir izin/engel listesi aracılığıyla hangi girdilerin sorgulanabileceğini belirtmeyi sağlayacak; hem tam girdiyi hem de bir regex'i kabul edecektir:

İzin verilen/reddedilen girdileri 'option' alanı için tanımlama

Ardından, izin verilen option'ı sorgulamak çalışırken, reddedilen option yalnızca null döndürecektir:

{
  # This option is allowed
  siteName: optionValue(name: "blogname")
  # This optionValue is not allowed
  adminEmail: optionValue(name: "admin_email")
}

GraphQL sunucusu tarafından sağlanan uygun güvenlik önlemleri ve geliştiricinin sağduyusuyla, güvenli bir GraphQL API oluşturmak zor olmamalıdır.

GraphQL'in Veritabanını Çöktürmesi

GraphQL, derin ilişkisel queries'in ifade edilmesine olanak tanıyan zengin bir söz dizimidir; bu nedenle WordPress gibi bir ekosistemde, veri modelinin genişletilebilirliğinin varlık-öznitelik-değer modelinden geldiği yerlerde, GraphQL queries'i derin, karmaşık veya özyinelemeli olduğunda sitenizin yanıt vermez hale gelmesine neden olabilecek inanılmaz miktarda aşınma ve yıpranmaya dönüşür. WordPress, MySQL/MariaDB örneğini dizleri üstüne çöktürme konusunda zaten ünlüdür; dolayısıyla queries düzgün yazılmamış, kimlik doğrulaması yapılmamış ve hız sınırlamasına tabi tutulmamışsa GraphQL eklemek işleri çok daha kötü yapabilir. [Kaynak]

Veritabanını çöktürmek, GraphQL sunucuları için ciddi bir endişedir. Gato GraphQL'in bu senaryodan nasıl kaçınmaya çalıştığını açıklayacağım.

Gato GraphQL, N+1 sorunununun hiç yaşanmasını önler; bu mimari tasarımla zaten sağlanmıştır. Bunu, varlıkları veritabanından yükleme sorumluluğunu geliştiriciye değil, motorun kendisine yükleyerek başarır.

Bir resolver'da bağlantılar çözümlenirken, döndürülen değer nesnenin kendisi değil, nesne(ler)in ID'si (veya ID listesi)dir. Örneğin, custom post'un yazarını almak şöyle yapılır:

class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
  private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
 
  public function getClassesToAttachTo(): array
  {
    return [
      CustomPostFieldInterfaceResolver::class,
    ];
  }
 
  public function getSchemaFieldType(string $fieldName): ?string
  {
    return match($fieldName) {
      'author' => SchemaDefinition::TYPE_ID,
      default => null,
    };
  }
 
  public function resolveValue(
    TypeResolverInterface $typeResolver,
    object $customPost,
    string $fieldName,
    array $fieldArgs = []
  ): mixed {
    switch ($fieldName) {
      case 'author':
        return $this->customPostUserTypeAPI->getAuthorID($customPost);
    }
 
    return null;
  }
 
  public function resolveFieldTypeResolverClass(
    TypeResolverInterface $typeResolver,
    string $fieldName
  ): ?string {
    switch ($fieldName) {
      case 'author':
        return UserTypeResolver::class;
    }
 
    return null;
  }
}

resolveValue'dan DB varlığının ID'sine ve resolveFieldTypeResolverClass'tan nesnenin tipine (sınıf UserTypeResolver aracılığıyla temsil edilen) sahip olan GraphQL motoru, nesne için verileri yükleyebilir.

Verileri yüklemek için motor son derece verimli bir algoritma kullanır: düğüm sayısı değil, queries'deki tip sayısı olan n ile O(n) zaman karmaşıklığına sahiptir.

Algoritma bu verimliliği, bir grafı dolaşmak yerine veri yapısını bir bileşen yığınına dönüştürmesiyle sağlar; bu çok daha basit çözümlenir. (GraphQL'deki "graph" bir kavramdır, gerçek bir uygulama değildir.)

Dolayısıyla, queries birçok varlık alan birden fazla seviyeye sahip olsa bile, algoritma bunu oldukça iyi kaldırabilir. Örneğin, 10 seviye derinliğe sahip aşağıdaki queries'i çalıştırmanın büyük bir etkisi yoktur:

{
  posts(pagination: { limit: 10 }) {
    excerpt
    title
    url
    author {
      name
      url
      posts(pagination: { limit: 10 }) {
        title
        tags(pagination: { limit: 10 }) {
          slug
          url
          posts(pagination: { limit: 10 }) {
            title
            comments(pagination: { limit: 10 }) {
              content
              date
              author {
                name
                posts(pagination: { limit: 10 }) {
                  title
                  url
                  comments(pagination: { limit: 10 }) {
                    content
                    date
                    author {
                      name
                      username
                      url
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Bu verimliliğin istisnası, Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue ve PostCategory.metaValue aracılığıyla meta değerleri alındığında ortaya çıkar (ve bunların metaValues alanı da dahil). Bunun nedeni, WordPress işlevlerinin (get_post_meta, get_user_meta, vb.) bir seferde 1 ID için veri çekmesidir; bu da her varlığın meta değerini almak için bir veritabanı çağrısı gerektireceği anlamına gelir. Sonuç olarak, meta değerleri çözümleme, tip sayısına değil düğüm sayısına göre ölçeklenir (bu konuda OP'nin yorumu tam isabetlidir).

Kötü niyetli kişilerin meta alanlarını kullanmasını ve kötüye kullanmasını önlemek için Gato GraphQL (v0.8'de) bu alanlarla varsayılan olarak devre dışı şekilde gelecektir. Ardından yönetici bunları açıkça etkinleştirmeli ve bunu yaparken bu alanları bir Erişim Kontrol Listesi altına yerleştirebilir; böylece DB hiçbir zaman saldırı riskiyle karşı karşıya kalmamalıdır.

Hız sınırlaması da harika bir fikir; yaklaşan bir sürümde desteklemeyi planlıyorum.

Bir de queries'in karmaşıklığını analiz etme ve sınırlamalar koyma meselesi var (kaç seviye derinliğe sahip olduğu gibi). GraphQL sunucusu queries'i O(n) zaman karmaşıklığıyla çözümler; dolayısıyla döngüler açısından yapılabilecek fazla hasar yoktur. Ancak tek bir queries hâlâ DB'den sınırsız miktarda veri alabilir ve bu önlemek isteyebileceğimiz bir şeydir.

Örneğin, bu basit queries tek bir istekte çok büyük miktarda veri getirecektir (demo sitemmde yalnızca birkaç yüz kayıt var, bu nedenle queries'i çalıştırmayı göstermeyi göze alabiliyorum):

{
  posts000: posts(pagination: { limit: 100 }) {
    ...PostFields
  }
  posts100: posts(pagination: { limit: 100, offset: 100 }) {
    ...PostFields
  }
  posts200: posts(pagination: { limit: 100, offset: 200 }) {
    ...PostFields
  }
  posts300: posts(pagination: { limit: 100, offset: 300 }) {
    ...PostFields
  }
  posts400: posts(pagination: { limit: 100, offset: 400 }) {
    ...PostFields
  }
  posts500: posts(pagination: { limit: 100, offset: 500 }) {
    ...PostFields
  }
  posts600: posts(pagination: { limit: 100, offset: 600 }) {
    ...PostFields
  }
  posts700: posts(pagination: { limit: 100, offset: 700 }) {
    ...PostFields
  }
  posts800: posts(pagination: { limit: 100, offset: 800 }) {
    ...PostFields
  }
  posts900: posts(pagination: { limit: 100, offset: 900 }) {
    ...PostFields
  }
}
 
fragment PostFields on Post {
  id
  title
  content
  date
}

Görülebileceği gibi, sorun yaratmak için queries'in iç içe geçmesi bile gerekmez. Bu nedenle bir queries'in karmaşıklığını analiz etmek zorlu bir iştir ve kullanışlı olabilmesi için ince ayar gerektirecektir.

Queries analizini de desteklemeyi umuyorum, ancak bu öncelik listemde yüksek sıralarda değil; çünkü diğer özelliklerinin bir kombinasyonuyla (Erişim Kontrol Listeleriyle birleştirilmiş persisted queries veya custom endpoints gibi) kötü niyetli kişileri zaten dışarıda tutabiliyoruz ve biz kendimiz kendi GraphQL servisimizi kötüye kullanmamalıyız (kullanmamalıyız!).


Bültenimize abone olun

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