Nesne hidratasyonu, siz onu ölçene kadar çözülmüş bir problem gibi görünür. Veritabanından bir satır çekersiniz, diziyi bir nesneye eşlersiniz ve devam edersiniz. Çoğumuz bunu görünmez, sıkıcı ve yeterince hızlı olduğu varsayılan bir tesisat işi gibi görürüz. Sonra bir gün bir toplu işi (batch job) veya bir kuyruk işleyicisini (queue worker) profillersiniz ve CPU süresinin şaşırtıcı bir kısmının mapper'da yok olduğunu fark edersiniz. Bu farkındalık, tam olarak HydraType'ın doğmasına yol açtı; HydraType, tek bir inatçı kural etrafında inşa edilmiş bir hydrator'dır: Bir sınıf, özellikleri kullanmadığı özelliklerin bedelini asla ödememelidir.
Eğer bir alan, sıfır dönüşüm gerektiren düz bir dize (string) ise, oluşturulan kod sanki bir insan tarafından elle yazılmış gibi görünmelidir. Doğrudan atama. Döngü yok, pipeline yok, reflection yok.
Hidratasyon Aslında Ne Kadara Mal Olur?
İlk bakışta, ['name' => 'Alice', 'age' => 30] verisini bir User nesnesine dönüştürmek basit görünür. Sorun, esneklik istediğinizde başlar. Çoğu genel amaçlı hydrator, sınıfları çalışma zamanında (runtime) incelemek için reflection'a güvenir. Metadata haritaları oluştururlar, tip dönüşümlerini (type coercion) yönetirler ve iç içe geçmiş nesne grafiklerini (nested object graphs) çözerler. Ayrıca pipeline'ları çok severler. O dizi boş olsa bile, her değer bir dizi mutator, assertion ve transformer içinden geçer. Döngü yapısının kendisi bile ek yük (overhead) getirir.
Birkaç düzine kayıt çekerseniz bunu asla fark etmezsiniz. Ancak modern PHP uygulamaları rutin olarak binlerce kuyruk mesajını işler, devasa CSV setlerini içe aktarır veya Elasticsearch'ten derin sonuç kümelerini hydrate eder. Bu senaryolarda, alan başına birkaç mikrosaniye bile yakan bir mapper, gerçek bir darboğaza (bottleneck) dönüşür. Sekiz alanı olan bin nesne, hissedeceğiniz bir vergiye dönüşür.
Sıfır Ek Yük (Zero-Overhead) Felsefesi
HydraType'ın arkasındaki temel fikir özellik izolasyonudur (feature isolation). Tip dönüşümü, iç içe nesne hidratasyonu ve assertion'ların hepsi desteklenir, ancak hiçbiri zorunlu değildir. Eğer bir özellik, diziden nesneye doğrudan bir kopyalamadan başka bir şeye ihtiyaç duymuyorsa, oluşturulan writer tam olarak bunu içerir. Düz bir skaler alanın, ihtiyaç duymadığı doğrulayıcıların (validators) arkasında sırada beklemesini zorlayan ortak bir pipeline yoktur.
Bu, göründüğünden daha zordur. Birçok kütüphane, kod tabanını küçük ve tek tip tutmak için her alan için tek bir yürütme yolu (execution path) paylaşır. HydraType tam tersini yapar. Her hedef DTO için benzersiz bir PHP sınıfı oluşturur ve tam olarak o sınıfın gerektirdiği işlemleri üretir. Karmaşıklık, her özellik için genel bir vergi değil, isteğe bağlı (opt-in) bir durum haline gelir.
Hız Nasıl Sağlanıyor?
HydraType'ı hem genel reflection tabanlı araçlardan hem de diğer oluşturulmuş yaklaşımlardan ayıran dört somut seçim vardır.
1. Kodu Bir Kez Oluştur, Sonsuza Dek Çalıştır
HydraType hedef sınıfınızı tek bir sefer inceler ve ardından onun tam şekline göre uyarlanmış özel bir PHP sınıfı yazar. Eğer DTO'nuz bir public string $name, bir int $status ve bir private DateTime $createdAt içeriyorsa, oluşturulan writer tüm bunları önceden bilir. Çalışma zamanında tekrarlanan reflection, string ayrıştırma (parsing) veya tembel (lazy) metadata oluşturma yoktur. OPCache oluşturulan dosyayı derlediğinde, Hydrator elle yazılmış PHP'den ayırt edilemez.
2. Boş Pipeline'ları Ortadan Kaldırın
Çoğu hydrator, işlerini bir runtime pipeline etrafında yapılandırır. Mutator'lar, validator'lar ve transformer'lardan oluşan bir çağrılabilir (callable) dizisi tutarlar ve her değer üzerinde dönerler. Bu diziler boş olsa bile foreach veya array_reduce hala çalışır. HydraType bunu tamamen ortadan kaldırır. Kod üretimi sırasında bir özelliğin gerçekte neye ihtiyaç duyduğunu kontrol eder. Eğer işlem listesi boşsa, $object->name = $data['name']; gibi düz bir atama üretir. Çalışma zamanında hiçbir şey üzerinde döngü kurulmaz, çünkü üretici (generator) düşünme işini zaten yapmıştır.
3. Reflection Yazımlarını Öldürmek İçin Closure Scoping Kullanın
Özel (private) özellikler, harici mapper'lar için klasik bir baş ağrısıdır. Alışılagelmiş kaçış yolu ReflectionProperty::setValue() yöntemidir, ancak bu yöntem doğrudan özellik erişimine kıyasla ağır bir maliyet getirir. HydraType, Closure::bind kullanarak bundan kaçınır. Oluşturulan writer, hedef sınıfa kapsamlanmış (scoped) closure'lar içerir; bu da reflection kullanmadan doğrudan private ve protected özelliklere erişmelerini sağlar. Bağlama (binding) işlemi inşa (construction) sırasında bir kez gerçekleşir; ondan sonra closure, yerel (native) hıza yakın bir hızda çalışır.
4. Writer'ı Yeniden Kullanın
Bazı kütüphaneler, hydrate ettikleri her bir nesne için yeni bir strateji veya reflection grafiği örneği (instantiate) oluşturur. HydraType writer'ını bir kez oluşturur ve ardından bu örneği binlerce nesne boyunca yeniden kullanır. Nesne başına maliyet, özellik değerlerini ayarlama ve sonucu döndürme gibi en temel seviyeye iner.
Sayılar
PHP 8.2'de fark oldukça belirgin:
- HydraType: 267.9 ns
- Ocramius GeneratedHydrator: 307.9 ns
- Symfony PropertyNormalizer: 8,499.0 ns
- Valinor: 10,755.2 ns
Symfony PropertyNormalizer ve Valinor güçlü araçlardır ancak genel amaçlıdırlar. PropertyNormalizer; format algılama, normalleştiriciler ve derin nesne grafikleriyle ilgilenen bir serializer bileşeninin parçasıdır. Valinor, tip ağaçları ve tip zorlama (coercion) konusunda titizdir. Bu gücün bir maliyeti vardır. Bu testte, HydraType'tan yaklaşık otuz ila kırk kat daha yavaş kaldılar. Halihazırda kod üretimi (code generation) kullanan Ocramius GeneratedHydrator bile, pipeline'lar ve özellik erişimi (property access) etrafındaki mimari farklılıklar nedeniyle biraz geride kalıyor.
Bağlam önemlidir. Tek bir veritabanı sorgusu veya bir HTTP gidiş-dönüşü (roundtrip) tüm bu sayıları gölgede bırakır. Bir sorgudan sonra sadece üç nesneyi hydrate ediyorsanız, nanosaniyelerin peşinden koşmak enerji kaybıdır. Fark, ölçeklendiğinizde anlam kazanır. Elli bin kaydı hydrate eden bir toplu işlem (batch process) veya nesneleri önbellek dizilerinden (cache arrays) yeniden oluşturan bir kuyruk tüketicisi (queue consumer), 267 nanosaniye ile on mikrosaniye arasındaki farkı hissedecektir. Alanlar ve satırlar boyunca bu farkı çarptığınızda, daha hızlı mapper, herhangi bir iş mantığını (business logic) değiştirmeden bir işten saniyelerce tasarruf sağlayabilir.
Asıl Çıkarım
Buradaki ders, her projenin özel bir hydrator'a ihtiyaç duyduğu değildir. Ders, mimari seçimlerin birikimli etkisidir. HydraType, yalnızca talep ettiğiniz şeyleri içeren kodlar üreterek düz özellikleri (plain properties) hızlı tutarken, karmaşık olanlar için hala bir kaçış yolu (escape hatch) sunar. Tip dönüşümü ve iç içe geçmiş nesneler ihtiyacınız olduğunda mevcuttur, ancak nesne üzerindeki her bir skaler string için performans kontrolüne ortak imza atmazlar.
Araç değiştirmeden önce profil çıkarın (profile). Eğer hydration gerçek dünya izlemelerinizde (traces) görünmüyorsa, düzeltilecek bir şey yoktur. Ancak büyük veri kümelerini genel amaçlı mapper'lar üzerinden geçiriyor ve CPU kullanımının artışını izliyorsanız, düz PHP atamasının (assignment) hala var olduğunu unutmayın. Bazen en hızlı kod, sadece kendinizin yazacağı, bir kez üretilip sonra unutulan koddur.
