🤔 Yeni Gato GraphQL neden 1,5 yılda yayınlandı?
Gato GraphQL'in 0.9 sürümü yeni yayınlandı. Hazır hale gelmesi için neredeyse 1,5 yıllık geliştirme süreci ve 16000'den fazla commit gerekti. Gerçekten uzun bir süre!
Haberi Hacker News'te paylaşınca, şu soruyu aldım:
[...] 16k commit'in neye harcandığını merak ediyorum. Üzerinde çalıştığım on binden fazla commit'e sahip projeler, tam zamanlı çalışan onlarca ya da yüzlerce kişiye sahipti. [...] Yazıda ele alınmayan, aşılması gereken bir karmaşıklık var mı?
Commit sayısı çok güvenilir bir ölçüt değildir; bazen çok basit bir değişiklik yapıp bunu tek bir commit olarak gönderebilirim. O 16 bin commit'in büyük çoğunluğu "typo" commit'leriydi ya da herhangi bir README dosyasındaki açıklamayı geliştirmekten ibaretti.
Yine de commit sayısı, harcanan gerçek çabaya dair bir fikir verir. Onlarca hatta yüzlerce değişikliği bir arada barındıran pek çok commit de vardı. 0.8 ve 0.9 sürümleri arasındaki değişiklikler gerçekten devasa, ve bunları hayata geçirmek ciddi çaba ve zaman gerektirdi.
Bu blog yazısında söz konusu değişikliklerin neler olduğunu anlatarak neden bu kadar uzun sürdüğünü açıklayacağım. Bunu yaparken, kod tabanına eklenen ve yaklaşan 1.0 sürümüyle gün yüzüne çıkacak bazı gelişmiş özelliklere de önizleme sunacağım.
GraphQL sunucusunun arka planı
Öncelikle, motorun geçmişinden ve nasıl çalıştığına dair teknik ayrıntılardan biraz bahsedeceğim.
(Bu bölüm ağırlıklı olarak geliştiricilerle ilgilidir; teknik konularla ilgilenmiyorsanız bir sonraki bölüme geçebilirsiniz.)
Gato GraphQL, PHP'de bileşenler oluşturan bir motor olan PoP'un üzerine inşa edilmiştir (JavaScript'teki React veya Vue'ya benzer). Bu motora bağımlılığı mutlak düzeydedir; bu nedenle plugin, GitHub'daki GatoGraphQL/GatoGraphQL monorepo'sunda barındırılmaktadır.
Bu bağımlılık perde arkasında şu şekilde görünür:
Gato GraphQL, bir GraphQL sorgusunu önce eşdeğer bir bileşen modeline dönüştürerek çözer; PoP bu modeli gerekli tüm verileri getirerek işler ve ardından bu veriler GraphQL sorgusunun şekline kavuşturulur.
2013/2014 yıllarında PoP üzerinde çalışmaya başladığımda GraphQL henüz yoktu; bir bileşen modelinin verilere dönüştürülmesi metodolojisi sıfırdan tasarlandı ve uygulandı. Takip edecek bir model olmaması (kavramlar için GraphQL ve bir uygulama için graphql-js referans projesi gibi), hem bir engel hem de bir lütuf oldu; bunu daha sonra açıklayacağım.
PoP başlangıçta tüm web sitesini sunucu tarafında HTML olarak oluşturmak, aynı zamanda sayfanın URL'sine ?output=json eklenerek ham verileri JSON formatında sunmak ve ek URL parametreleriyle hangi verilerin alınacağını (ayarlar, DB nesne verileri) seçmek amacıyla tasarlandı.
Aşağıdaki bağlantılara tıklayın (hepsi aynı web sayfasına, yalnızca farklı URL parametreleriyle işaret eder) ve nasıl farklılaştıklarına dikkat edin:
- HTML içeriği: mesym.com/en/posts/
- Ham JSON verisi (ayarlar + DB): mesym.com/en/posts/?output=json
- Ham JSON verisi (DB): mesym.com/en/posts/?output=json&module=data
Son bağlantıya tıklandığında bir şey fark edilir: Bu neredeyse GraphQL! Tek büyük fark, yanıttaki verilerin örtük olmasıdır; çünkü sayfaya dahil edilen bileşenler (PHP'de) tarafından zaten tanımlanmışlardır. GraphQL ise bir sorgu aracılığıyla hangi verilerin alınacağını bizim belirlemememize olanak tanır.
2019 civarında GraphQL'i öğrendiğimde, PoP'un bir GraphQL sunucusu olarak da işlev görmesi benim için son derece mantıklıydı. Tek yapması gereken, GraphQL sorgusunu girdi olarak kabul etmek ve sorguya dayalı olarak anında bir bileşen modeli oluşturmaktı.
İşte tam olarak bunu yaptım. Ve iyi çalıştı. Ancak yavaştı; çünkü PoP kendi girdi formatını anlıyordu, dolayısıyla GraphQL sorgusunun PoP formatına dönüştürülmesi gerekiyordu:
- GraphQL sorgusunu ayrıştır; ardından
- Sorguyu PoP formatına dönüştür; ardından
- PoP formatını ayrıştır
GraphQL sorgusunun ayrıştırılması böylece iki kez yapılıyordu (bir kez GraphQL için, bir kez PoP için) ve PoP formatı bir AST aracılığıyla değil, sorgu dizesini tekrar tekrar ayrıştırarak işleniyordu. (AST kullanmamak berbat bir kodlama pratiğiydi; ancak takip edecek bir spesifikasyonum yoktu ve geliştirme süreci organik bir şekilde ilerliyordu; basit bir substr(...) her gün işe yarıyordu.)
GraphQL spesifikasyonuna sahip olmamanın bir engel olduğunu söylememin nedeni budur; çünkü çözümüm yavaştı (bu durum 0.8 sürümündeydi). Bu yüzden bunu düzeltmeye karar verdim.
Motoru GraphQL-first'e dönüştürmek
Seçtiğim çözüm, PoP'un GraphQL dilini doğrudan konuşmasını sağlamaktır. Bu sayede PoP'a girdi olarak bir GraphQL sorgusu iletmek, ek bir adaptöre gerek kalmadan ve işlemleri iki kez yapmadan bileşen modeline dönüştürülecekti.
Bu, PoP projesinin yeniden amacına kavuşturulması anlamına geliyordu: GraphQL sorgularını çözümlemek üzere uyarlanmış, sunucu tarafında web siteleri için bileşenler oluşturan bir PHP kütüphanesinden, gerçek bir GraphQL sunucusuna dönüşmesi gerekiyordu.
Kod tabanı bunun üzerine devasa bir dönüşüm geçirdi; motordaki tüm PHP servisleri arasında durumu iletmek için GraphQL AST temel alındı. GraphQL AST nesneleri artık PoP'un girdileridir (sorgu dizeleri yerine).
PHP'deki diğer GraphQL sunucuları graphql-php'ye güvenir; ancak Gato GraphQL plugin'i buna dayanmaz. Bu, bakım çabası açısından kötü bir haberdir (başkasının yazdığı kodu yeniden kullanamazsınız), ancak bağımsızlık açısından iyi bir haberdir: kendi hızımda ve kendi kriterimle plugin'ime özel özellikler eklemeye karar verebilirim (bu nedenle plugin halihazırda "oneof" girdi nesnesini sunmaktadır).
Aşağıdaki bölümde gösterileceği üzere, bu büyük bir avantajdır.
GraphQL'e özgün özellikler eklemek
GraphQL normalde veri getirmeyle ilişkilendirilir. Elbette Gato GraphQL aracılığıyla her türlü veriyi (yazılar, kullanıcılar, yorumlar vb.) alabilirsiniz:
query {
posts(
pagination: { limit: 5, offset: 20 }
sort: { by: DATE, order: ASC }
) {
id
title
content
url
author {
id
name
url
}
comments {
id
date
content
}
}
}Ancak bu yalnızca kolay bir başlangıç noktasıdır. GraphQL, veri manipülasyonu ve dönüştürme dahil pek çok başka kullanım senaryosu için de kullanılabilir; hatta servisler arasında aracılık yapmak amacıyla GraphQL'i bir pipeline'a dahil etmek bile mümkündür.
GraphQL'in yararlı olduğu bazı örnekler şunlardır:
- Bir veya birden fazla kaynaktan bilgi çıkarmak (WordPress sitelerindeki kullanıcılar ve Mailchimp'teki bülten iletişim verileri gibi), verileri birleştirmek ve hepsini tek bir veri kümesi olarak analiz etmek
- Sitedeki içeriği uyarlamak için işlemler gerçekleştirmek:
- Bir siteyi başka bir alana taşırken
"www.myoldsite.com"ifadesini içerik ve meta verilerin her yerinde"mynewsite.com"ile değiştirmek gibi tek seferlik işlemler - Bir yazar yeni bir blog yazısı yayınladığında
"http://"ifadesini"https://"ile değiştirmek gibi sürekli işlemler
- Bir siteyi başka bir alana taşırken
- Tüm blog yazılarını farklı bir dile çevirmek için Google Translate API'sine bağlanmak
- Bir blog yazısı yayınlandıktan sonra otomatik olarak bir tweet göndermek
PoP, GraphQL tarafından (doğal olarak) desteklenmeyen özellikler aracılığıyla bu diğer kullanım senaryolarını desteklemek üzere tasarlanmıştı:
- Şemadaki tüm türlere eklenen "işlevsellik" alanlarını (ek olarak "veri" alanlarına) desteklemek
- Bir alanın sonucunu aynı sorgu içinde başka bir alana girdi olarak iletmek
- Bir directive'in başka bir directive'in davranışını değiştirmesi için directive'leri birleştirmek
- Alanın değerine göre dinamik olarak bir directive'in uygulanıp uygulanmayacağına karar vermek
Bu özellikleri GraphQL sunucusundan kaldırmak kesinlikle istemiyordum: onları zaten kodlamıştım ve gerçekten değerlilerdi.
Dolayısıyla v0.9'un bu kadar uzun sürmesinin ikinci nedeni, bu yeni yetenekleri GraphQL'e dahil etmenin bir yolunu da bulmam gerektiğiydi; üstelik GraphQL spesifikasyonunu kırmayan bir şekilde (örneğin, GraphQL söz dizimine yeni öğeler eklemek söz konusu değildi).
GraphQL'de veri manipülasyonu örneği
Plugin tarafından GraphQL'e tanıtılan yeni yetenekler, 1.0 sürümü yayınlandığında yakın gelecekte daha görünür hale gelecek. Ancak bunların bir kısmını şimdiden deneyimleyebilirsiniz.
Aşağıdaki GraphQL sorgusu, bir dış REST API'sinden kullanıcı girişlerinin listesini alır (yanıttan @remove ile kaldırılabilir); bu verileri aynı sorgu içinde başka bir alana girdi olarak iletir; her girişten e-posta özelliğini çıkarır; ve son olarak, yalnızca aynı girişin dili İngilizce veya Almanca ise e-postayı büyük harfe dönüştürür:
###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes
{
# Retrieve data from a REST API endpoint
userEntries: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
}
) # @remove # <= Uncomment this directive to not print the API data
emails: _echo(value: $__userEntries)
# Iterate all the entries, passing every entry
# (under the dynamic variable $userEntry)
# to each of the next 4 directives
@underEachArrayItem(
passValueOnwardsAs: "userEntry"
affectDirectivesUnderPos: [1, 2, 3, 4]
)
# Extract property "lang" from the entry
# via the functionality field `_objectProperty`,
# and pass it onwards as dynamic variable $userLang
@applyField(
name: "_objectProperty"
arguments: {
object: $userEntry,
by: {
key: "lang"
}
}
passOnwardsAs: "userLang"
)
# Execute functionality field `_inArray` to find out
# if $userLang is either "en" or "de", and place the
# result under dynamic variable $isSpecialLang
@applyField(
name: "_inArray"
arguments: {
value: $userLang,
array: ["en", "de"]
}
passOnwardsAs: "isSpecialLang"
)
# Extract property "email" from the entry
# and set it back as the value for that entry
@applyField(
name: "_objectProperty"
arguments: {
object: $userEntry,
by: {
key: "email"
}
}
setResultInResponse: true
)
# If $isSpecialLang is `true` then execute
# directive `@strUpperCase`
@if(condition: $isSpecialLang)
@strUpperCase
}İşte yanıt (yalnızca bazı e-postaların büyük harfe dönüştürüldüğüne dikkat edin):
{
"data": {
"userEntries": [
{
"email": "abracadabra@ganga.com",
"lang": "de"
},
{
"email": "longon@caramanon.com",
"lang": "es"
},
{
"email": "rancotanto@parabara.com",
"lang": "en"
},
{
"email": "quezarapadon@quebrulacha.net",
"lang": "fr"
},
{
"email": "test@test.com",
"lang": "de"
},
{
"email": "emilanga@pedrola.com",
"lang": "fr"
}
],
"emails": [
"ABRACADABRA@GANGA.COM",
"longon@caramanon.com",
"RANCOTANTO@PARABARA.COM",
"quezarapadon@quebrulacha.net",
"TEST@TEST.COM",
"emilanga@pedrola.com"
]
}
}Kendiniz deneyin! Sorguyu çalıştırmak için "Run" düğmesine basın:
###################################################################
# Fetch data from a REST endpoint, extract the emails, and make
# uppercase those ones from users with a special language.
###################################################################
query ExtractEmailsFromAPIAndUpperCaseSpecialOnes {
# Retrieve data from a REST API endpoint
userEntries: _sendJSONObjectCollectionHTTPRequest(
input: {
url: "https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"
}
)
# @remove # <= Uncomment this directive to not print the API data
emails: _echo(value: $__userEntries)
# Iterate all the entries, passing every entry
# (under the dynamic variable $userEntry)
# to each of the next 4 directives
@underEachArrayItem(
passValueOnwardsAs: "userEntry"
affectDirectivesUnderPos: [1, 2, 3, 4]
)
# Extract property "lang" from the entry
# via the functionality field `_objectProperty`,
# and pass it onwards as dynamic variable $userLang
@applyField(
name: "_objectProperty"
arguments: { object: $userEntry, by: { key: "lang" } }
passOnwardsAs: "userLang"
)
# Execute functionality field `_inArray` to find out
# if $userLang is either "en" or "de", and place the
# result under dynamic variable $isSpecialLang
@applyField(
name: "_inArray"
arguments: { value: $userLang, array: ["en", "de"] }
passOnwardsAs: "isSpecialLang"
)
# Extract property "email" from the entry
# and set it back as the value for that entry
@applyField(
name: "_objectProperty"
arguments: { object: $userEntry, by: { key: "email" } }
setResultInResponse: true
)
# If $isSpecialLang is `true` then execute
# directive `@strUpperCase`
@if(condition: $isSpecialLang)
@strUpperCase
}GraphQL tarafından yönlendirilmemenin bir engel olduğundan, ancak (geriye dönüp bakıldığında) aynı zamanda bir lütuf olduğundan bahsetmiştim. Bunun nedeni, GraphQL spesifikasyonunun kısıtlamalarına sahip olmamamdı; dolayısıyla bu yeni yetenekleri hayal edebilme lüksüne sahiptim.
Ve artık bu özellikler Gato GraphQL'e taşındığına göre, WordPress siteniz için içerik alma, manipülasyon ve dönüştürme konusundaki her şeyde inanılmaz derecede yararlı bir müttefik olabilir. (Bunlara yalnızca yaklaşan v1.0 ile erişilebilecek olsa da.)
Zaman almıştı, ama harcanan çaba kesinlikle buna değdi.
Deneyin!
Uzun beklemenin buna değdiğine ikna oldunuz mu? Umarım öyle!
Hemen indirin ve bir göz atın:
Geliştirme süreci, yeni belgeler ve v1.0 dahil yaklaşan sürümlerle ilgili haberler almak ister misiniz? O zaman bültene abone olmaya davetlisiniz.
GitHub'daki açık kaynak kodunu keşfetmek ister misiniz? GatoGraphQL/GatoGraphQL'e göz atın (ve bir yıldız bırakmaktan çekinmeyin... Yıldızları seviyoruz! ⭐️⭐️⭐️)
Bu arada, WordPress'te hangi içerik dönüşümlerine ihtiyaç duyuyorsunuz (bunun için halihazırda özel bir ticari plugin kullanıyor olabilirsiniz)? Lütfen kullanım senaryonuzu anlatan bir mesaj gönderin.
Gördüklerinizi beğendiyseniz, lütfen arkadaşlarınız ve meslektaşlarınızla paylaşın, sevgiyi yaymaya yardımcı olun ❤️.