Container Derlemesi Nasıl İşler
Bu sayfa container derlemesini açıyor: hangi aşamalardan geçtiğini, yapılandırma parametrelerinin ne zaman
genişletildiğini, @service dizelerinin ne zaman gerçek referanslara dönüştüğünü ve extension yazarlarının
en çok sorduğu soruyu: hangi aşamada servisleri türe göre güvenle arayabileceğinizi. Extension yazma bölümünün derinlemesine
tamamlayıcısıdır.
Sıradan bir uygulama, hatta sıradan bir extension yazmak için bunların hiçbirine ihtiyacınız yok. Ama extension'ınız
servis grafiğini incelemeye ya da yeniden biçimlendirmeye başladığında zamanlama her şey olur: aynı
getByType() çağrısı bir aşamada güvenilir, başka bir aşamada yanıltıcı bir yanıt verir. Bu sayfa nedenini
açıklıyor; böylece kodunuzun nereye ait olduğunu her zaman bilirsiniz.
İki Dünya: Derleme ve Çalışma Zamanı
Anlaşılması en önemli şey, bir Nette container'ının her istekte kurulmadığıdır. Bir kez, iyileştirilmiş bir
PHP sınıfı olarak kurulur, bu sınıf diske yazılır ve sonraki her istek yalnızca bitmiş dosyayı include
eder. Aşağıda anlatılan tüm düzenek (extension'lar, çözücüler, kod üreteci) yalnızca (yeniden) derleme
sırasında çalışır.
Bu, dünyayı asla bir arada bulunmayan iki temsile ayırır:
| derleme sırasında | çalışma zamanında | |
|---|---|---|
| Var olan | ContainerBuilder içindeki tanımlar (tarifler) |
Container içindeki servis örnekleri |
| Temel sınıflar | Compiler, ContainerBuilder, Resolver, PhpGenerator |
Container (üretilen sınıfın atası) |
%param%, @service |
hâlâ çevrilen metinsel işaretler | çevrilmiş / koda gömülmüş |
Üretilen sınıf Nette\DI\Container sınıfını genişletir ve her servis için bir
createServiceXxx() metodu içerir. Parametreleri ve autowiring meta verileri önceden hesaplanır; böylece
çalışma zamanında çözülecek bir şey kalmaz, yalnızca servisler istendikçe örneklenir.
Geliştirici kipinde container, bir yapılandırma dosyası ya da extension sınıfı değiştiğinde otomatik olarak yeniden kurulur; her ikisi de bağımlılık olarak izlenir. Üretimde bir kez derlenir ve bir daha denetlenmez; hız da buradan gelir.
Aşamalara Kuşbakışı
Derlemeyi Compiler::compile() yönetir ve üç adıma iner:
public function compile(): string
{
$this->processExtensions(); // AŞAMA A: şemalar + loadConfiguration()
$this->processBeforeCompile(); // AŞAMA B: resolve + beforeCompile() + complete
return $this->generateCode(); // AŞAMA C: kod üretimi + afterCompile()
}
Tüm zihinsel model tek bir düşünceye sığar: her aşama bir öncekinden daha çok şey bilir.
- Aşama A grafiği tanımlarla doldurur. Servis türleri henüz güvenilir biçimde bilinmez; çünkü bir tür, henüz kimsenin bakmadığı bir factory'nin dönüş değerinden gelebilir.
- Aşama B önce tüm türleri çözer (
resolve), sonra extension'ların grafiği yeniden biçimlendirmesine izin verir (beforeCompile) ve en sonunda argümanları autowiring ile bağlar (complete). - Aşama C bitmiş grafiği PHP'ye çevirir ve extension'ların üretilen koda dokunmasına izin verir.
Bilginin böyle büyümesi, aynı işlemin neden bir aşamada güvenli, başka bir aşamada güvenilmez olduğunun tam nedenidir. Sayfanın geri kalanı aşamaları bu düşünceyle geziyor.
Aşama A: Tanımların Kaydedilmesi
Bu aşamada Nette her extension'da üç metot çağırır (getConfigSchema(), sonra setConfig(),
sonra loadConfiguration()), ama özenle denetlenen bir sırayla; çünkü burada sıra gerçekten
önemlidir.
Sıra Neden Önemli
ParametersExtensionveExtensionsExtensionönce gelir. Birincisi,%param%ifadelerini yapılandırmanın tamamında genişletebilmek için her şeyden önce çalışmalıdır; diğer her extension kendi bölümünü değerleri doldurulmuş olarak alır. İkincisi,extensions:bölümünde listelenen başka extension'ları kaydeder; dolayısıyla o da geri kalanlar işlenmeden önce var olmalıdır.ServicesExtensionen sona kalır. Böylece kullanıcınınservices:bölümü her zaman son sözü söyler ve extension'ların kurduğu her şeyi geçersiz kılabilir.InjectExtensionen sona taşınır; böylece işi, diğer tüm extension'ların eklediği setup'ları görür.
Sizin için çıkarım şu: extension'ınızın loadConfiguration() metodu çalıştığında parametreler zaten
genişletilmiştir, ama kullanıcının servisleri henüz ortada yoktur. Aşağıdaki zamanlama kurallarının çoğunu bu tek
gerçek belirler.
services: Bölümünün Tanımlara Dönüşmesi
Kullanıcının services: bölümü, aşama A'nın son adımında burada tanım nesnelerine dönüştürülür. Her
NEON girdisi normalleştirilir (kısa yazımlar tek biçime getirilir), türü saptanır (sıradan servis, factory, accessor, …)
ve builder'da uygun bir tanım oluşturulur. Basit @name / @Type argümanlarının referansa
dönüştüğü ilk an da budur; bkz. aşağısı.
Aşama A'nın sonunda tüm tanımlar yerindedir; her extension ve kullanıcı istediğini kaydetmiştir. Ama resim henüz net değildir:
- türü bir factory'nin dönüş değerinden gelen tanımlarda türler çözülmemiştir,
- argümanlar autowiring ile bağlanmamıştır,
- bazı
@servicereferansları hâlâ düz dizedir.
Türe göre aramanın burada neden güvenilmez olduğunun nedeni tam da budur; ayrıntısı aşağıda.
Parametreler: %param% Ne Zaman Genişletilir
İki başlıca sorudan biri. Yanıt kısa: bir kez, aşama A'nın en başında, yapılandırma ağacının tamamında.
ParametersExtension önce çalışır ve ilk yaptığı şeylerden biri %param% yer tutucularını
genişletmektir; önce parametrelerin kendi içinde (bir parametre bir başkasına başvurabilir), sonra yapılandırmanın geri
kalanında. Yani ServicesExtension dahil başka herhangi bir extension kendi bölümünü aldığında yer tutucular
çoktan gitmiştir. Extension'lar somut değerlerle çalışır, asla %...% ile değil.
Bir yer tutucu dizenin tamamıysa, değeri diziler ve nesneler dahil olduğu gibi döndürülür; dolayısıyla
%mailer% bütün bir diziye genişleyebilir. Başka her yerde bir dizeye birleştirilir ve noktalı yazım
%foo.bar% iç içe dizilere uzanır.
Statik ve Dinamik Parametreler
Her değer koda gömülemez. Değeri ortama göre değişen bir parametre (bir ortam değişkeni, istekten türetilen
baseUrl) dinamik kalmalıdır. Böyle parametreleri setDynamicParameterNames() ile ya da bir
şemada Expect::...->dynamic() ile bildirirsiniz; ayrıntısı Dinamik parametreler bölümünde.
Dinamik bir parametrenin yerine bir değer değil, onu çalışma zamanında okuyan bir ifade konur. Yani
%env.DB_HOST% bir dizeye donmaz; üretilen container'da çalışma zamanı aramasına dönüşür. Geri kalan her
şey statiktir ve derleme zamanında dondurulur; “getenv() değerim her ortamda aynı” şaşkınlığının
olağan kaynağı da budur: parametre yalnızca statikti.
Bunun tersi işlem kaçışlamadır: harfi harfine bir % ya da @ karakterinin yorumlanmasını
önlemek için iki katına çıkarılır (%%, @@). Nette bunu sizin için enjekte ettiği parametrelerde
otomatik yapar; böylece değerleri asla yer tutucu ya da referans sanılmaz.
Referanslar: @service Ne Zaman Referans Olur
İkinci başlıca soru. @service çevirisi, dizenin ne kadar karmaşık olduğuna göre farklı aşamalara
yayılan birkaç adımda gerçekleşir. Bunu elle izlemeniz nadiren gerekir, ama adımları bilmek bazı referansların neden
diğerlerinden önce çözüldüğünü açıklar.
- Ayrıştırma (yapılandırmanın yüklenmesi). Varlık olarak kullanılan bir
@service(servisi oluşturan şey,Foo(@bar)örneğindeki gibi) hemen referansa dönüşür. Argüman olarak kullanılan bir@serviceşimdilik düz dize kalır. Tırnak içindeki bir@,@@olarak kaçışlanır; böylece referans değil, harfi harfine metin sayılır. - Aşama A (
loadConfiguration). Tanımlar işlenirken, temiz bir@nameya da@Typeargümanı birReferencenesnesine dönüştürülür. Bu yalnızca basit biçimleri yakalar;@service::CONSTya da daha büyük bir ifadenin içindeki bir@sonraya bırakılır. - Aşama B (
complete). Asıl “akıllı” çeviri burada olur:@service→ referans,@service::CONSTANT→ harfi harfine bir sınıf sabiti,@service::property→ o özelliğin okunması,@@x→ harfi harfine@xmetni.
Referans sözcüğünün kendisinde gizli ikinci bir çeviri daha var. Bir Reference ya ada ya da
türe (@Namespace\Type) işaret edebilir. Bir tür referansı henüz bir servis adı değildir; somut
bir ada autowiring tarafından çözülür ve bu yalnızca complete adımında, autowiring dizini kurulduktan sonra
gerçekleşir. Bu, bir sonraki bölüme köprüdür: autowiring aramaları, dizin hazır olana dek bilinçli olarak ertelenir.
| Biçim | Şurada referansa/ifadeye dönüşür | Şurada somut servise çözülür |
|---|---|---|
varlık (factory olarak @foo) |
ayrıştırma | complete |
argüman @foo, @Type |
aşama A | complete |
@foo::CONST, @foo::prop |
aşama B | complete |
tür referansı @Type |
aşama A/B | complete (autowiring) |
ContainerBuilder'ı İnceleme: Ne Zaman Güvenli
Şimdi extension yazarlarının en çok sorduğu soru: hangi metotta servisleri türe göre arayabilirim? Yanıt, builder'ın kendi durumunu nasıl izlediğine dair basit bir kuraldan çıkar.
Türe göre arama (getByType(), getDefinitionByType(), findByType()), servis
grafiğinin çözülmüş olmasını gerektirir: her tür bilinmeli, autowiring dizini kurulmuş olmalı. Bu yüzden
bunlardan birini çağırdığınızda ve grafik son çözümden bu yana değiştiyse, builder bilinen grafiğin tamamını
anında çözer. Çözümün kendisi sırasında türe göre her arama yasaktır ve
NotAllowedDuringResolvingException fırlatır.
Etikete göre arama (findByTag()) böyle bir gereksinim taşımaz; etiketler türlere bağlı değildir,
dolayısıyla her aşamada çalışır.
Aşama aşama:
loadConfiguration()(aşama A) – türe göre arama güvenilmez. Grafik eksiktir: sonra çalışan extension'lar servislerini henüz kaydetmemiştir ve her şeyden önce kullanıcınınservices:bölümü (en sonda çalışır) ortada yoktur. BirgetByType()çağrısı işe yarar (kısmi grafiğin erken çözülmesini tetikler), ama yanıt eksik bir resimden gelir ve erken çözüm boşuna emek harcar. Kural:loadConfiguration()içinde yalnızca tanım kaydedin; türe göre aramayın.findByTag()sorun değil.beforeCompile()(aşama B) – inceleme için doğru yer. Bu noktada tüm tanımlar vardır (kullanıcınınkiler dahil), türler çözülmüştür ve autowiring dizini kurulmuştur; dolayısıylagetByType(),findByType()vefindByTag()güvenilir yanıtlar verir. Argümanlar henüz autowiring ile bağlanmamıştır; bu, tümbeforeCompile()çağrılarından sonraki adımdır (complete). Burada bir tanımı değiştirdiğinizde, sonrakigetByType()grafiği saydam biçimde yeniden çözer; böylece düzenlemelerle sorguları rahatça değiştirebilirsiniz.afterCompile()(aşama C) – yalnızca kod. Builder üzerinde değil, üretilen sınıf üzerinde çalışır. Grafik bitmiştir; burada sonuç PHP kodunu biçimlendirirsiniz.
| İstediğim… | Aşama |
|---|---|
| bir servis kaydetmek | loadConfiguration() |
| etikete göre arayıp tanımları değiştirmek | loadConfiguration() ya da beforeCompile() |
türe göre aramak (getByType/findByType) |
beforeCompile() |
| autowiring'in argümanlar için hangi servisleri seçtiğine dayanmak | derleme zamanında değil; çalışma zamanında inceleyin |
| üretilen koda dokunmak | afterCompile() |
| container başladıktan sonra kod çalıştırmak | başlatma kodu |
Aşama B'nin İçi: resolve ve complete
Aşama B, aralarına beforeCompile() çağrıları sıkışmış iki geçiştir:
$this->builder->resolve(); // türler çözülür, autowiring dizini kurulur
foreach ($this->extensions as $extension) {
$extension->beforeCompile();
}
$this->builder->complete(); // argümanlar ANCAK ŞİMDİ autowiring ile bağlanır
resolve(), her servisin türünü belirler (bildirilmiş type değerinden alır ya da
factory'sinden çıkarır: bir factory metodunun dönüş türü, örneklediği sınıf ya da bir referansın işaret ettiği
servis) ve ardından her türü (sınıfı, atalarını ve arayüzlerini) bir servis adına eşleyen autowiring dizinini kurar.
autowired: false işaretli bir servis dizine alınmaz; autowired: [A, B] ise onun görünür olduğu
türleri daraltır. Can alıcı nokta: resolve türleri belirler, argümanları değil; argümanların
autowiring'i bitmiş bir dizin gerektirir ve bu dizin ancak bu geçişten sonra vardır.
complete(), argümanların autowiring'inin asıl gerçekleştiği yerdir. Her tanım için eksik yapıcı
ve setup argümanlarını, türlerini artık tamamlanmış dizinde arayarak doldurur. Tür referanslarının resolve sırasında
çözülmeden bırakılmasının nedeni budur: arama buraya aittir, bakılacak güvenilir bir dizin oluştuktan sonra.
Aşama C: Kodun Üretilmesi
generateCode(), bitmiş grafiği PhpGenerator sınıfına verir; o da Container
sınıfını genişleten, her servis için bir createServiceXxx() metodu içeren ve önceden hesaplanmış
aliases, tags ile wiring meta verilerini taşıyan bir sınıf üretir. Her
Statement PHP metnine (new Foo(...), metot çağrıları, özellik erişimi), her Reference
ise bir $this->getService(...) çağrısına dönüşür.
Extension'lar sonra üretilen sınıf üzerinde son bir afterCompile() geçişi elde eder; örneğin statik ve
dinamik parametre getter'ları burada üretilir. Ayrıca her istekte çalışan başlatma kodu ekleme fırsatı da
burada doğar.
Zaman Çizelgesi Tek Bir Resimde
DERLEME (bir kez, önbelleğe)
│
├─ yapılandırma dosyalarını yükle NEON -> Statement/dizi; dosyaları birleştir
│ tırnaklı @ -> @@ ; varlıklar -> Statement
│
▼ Compiler::compile()
│
├─ AŞAMA A processExtensions()
│ ├─ ParametersExtension (İLK) ─── %param% tüm yapılandırmada GENİŞLETİLİR
│ │ dinamik olanlar -> çalışma zamanı ifadesi
│ ├─ ExtensionsExtension (İLK) ──── başka extension'ları kaydeder
│ ├─ ...diğer extension'lar... ── loadConfiguration(): yalnızca tanım kaydet
│ └─ ServicesExtension (SON) ── services: -> Definition nesneleri
│ @name/@Type -> Reference
│ [grafik sayıca tam; TÜRLER ve ARGÜMANLAR değil; türe göre arama güvenilmez]
│
├─ AŞAMA B processBeforeCompile()
│ ├─ builder.resolve() ── tüm türleri çöz; autowiring dizinini kur
│ │ [türler hazır; dizin hazır]
│ ├─ beforeCompile() extension ── getByType/findByType/findByTag BURADA GÜVENLİ
│ │ (argümanlar henüz bağlanmadı)
│ └─ builder.complete() ── ARGÜMANLARI bağla; referans çevirisini bitir
│ tür referansları -> servis adları
│
└─ AŞAMA C generateCode()
├─ PhpGenerator.generate() ── Statement -> PHP; createServiceXxx() metotları
├─ afterCompile() extension ── kodu ayarla; parametre getter'larını üret
└─ toString() ── son PHP kodu -> önbellek
────────────────────────────────────────────────────────────
ÇALIŞMA ZAMANI (her istek)
│
├─ new Container($dynamicParams)
├─ initialize() ── extension'ların açılış kodu (oturum, header, doğrulama)
└─ getService()/getByType() ── önceden hesaplanan meta veriden tembel örnekler
Yaygın Yanlış Anlamalar
- “
loadConfiguration()içinde servisleri türe göre ararım.” Hayır; grafik eksiktir (kullanıcınınservices:bölümü sizden sonra çalışır) vegetByType()kısmi bir grafiğin erken çözülmesini tetikler. BunubeforeCompile()metoduna taşıyın.findByTag()burada bile sorun değil. - “Bir parametredeki
getenv()değeri her ortamda farklı olur.” Yalnızca parametre dinamikse. Aksi hâlde derleme zamanında koda gömülür ve her yerde aynı kalır. - “
@Typereferansı zaten bir servis adıdır.” Değildir; bir tür referansıdır ve somut bir ada ancak complete adımında autowiring tarafından çözülür. - “Extension'ım bir yardımcı dosyayı okuyor, ama değişiklikler görünmüyor.” Onu
$builder->addDependency($file)ile kaydedin; aksi hâlde önbellek ondan habersiz kalır ve yeniden kurmaz. - “
resolve()sırasındagetByType()çağırabilirim.” Hayır;NotAllowedDuringResolvingExceptionfırlatır. Türe göre aramabeforeCompile()metoduna ya da sonrasına aittir, asla çözümün ortasına değil.