Bir sipariş listesi sayfası düşünün. Tek işi var: siparişleri ekrana basmak. Örnek bir servis grafiği kurup kurucuları saydım — o tek istek boyunca yedi nesne kuruldu, ikisi kullanıldı. Beşi, hiç dokunulmadan çöpe gitti.
PHP tarafında DI container tam bu noktada hem çözüm hem suç ortağıdır. Bağımlılıkları sizin yerinize kurar, ama kaç tane kurduğunu söylemez. Bağımlılık enjeksiyonunun kendisini daha önce yazmıştık; orada elle bir Di sınıfı da kurmuştuk. Bu yazı bir adım sonrası: container’ın kapağını açmak, yansımanın (reflection) faturasını ölçmek ve PHP 8.4’ten beri motorun bize bedavaya verdiği şeyi kullanmak.
Metafor matruşka. Dıştaki bebeği alıyorsunuz, içinden bir tane daha çıkıyor, onun içinden bir tane daha. Container’ın tek işi, hangi bebeğin hangisinin içine girdiğini bilmek. Tek derdi de şu: bütün bebekleri açmak zorunda değilken açıyor.
DI container nedir, bağımlılık enjeksiyonundan farkı ne?
Kısa tanım: DI container, hangi sınıfın hangi bağımlılıklarla kurulacağı bilgisini tek bir yerde tutan ve istendiğinde o nesneyi kurup veren nesnedir.
Bağımlılık enjeksiyonu bir tasarım kararıdır: sınıf, ihtiyacı olanı kendisi yaratmaz, dışarıdan alır. Container ise bir araçtır. İkisi aynı şey değil ve biri diğerini gerektirmiyor. Kurucusuna PDO alan bir sınıf yazdıysanız DI yapıyorsunuz, container’ınız olmasa bile.
Container’ı, elle kurmak dayanılmaz hâle geldiğinde alıyorsunuz. O eşiğin nerede olduğunu görmek için önce elle kuralım.
Elle kurulum nerede kopuyor?
// Sipariş listesi sayfası için elle kurulum
$config = new Config();
$logger = new Logger($config);
$database = new Database($config, $logger);
$pdf = new PdfRenderer($logger);
$mail = new MailQueue($database, $logger);
$service = new ReportService($database, $pdf, $mail);
$controller = new ReportController($service, $logger);
echo $controller->index();
Yedi satır, yedi nesne. Çalışıyor, okunuyor, hiçbir paket istemiyor. Buraya kadar iyi arkadaşlar. Sorun satır sayısında değil, sırada: Database’i Logger’dan önce kuramazsınız, Logger’ı da Config’den önce. Grafiği siz ezberliyorsunuz.
Acı Gerçek: Bu blok giriş noktası başına bir kez yazılır. İki giriş noktanız varsa iki kez. Kırk tane varsa, Logger’ın kurucusuna bir parametre eklediğiniz gün kırk dosya açıyorsunuz. Container’ın çözdüğü asıl problem performans değil, bu.
Yüz satırlık container: autowiring nasıl çalışıyor?
Autowiring (otomatik bağlama), container’ın kurucu parametrelerinin tip ipuçlarını okuyup gerekli nesneleri kendisinin kurması demek. Tamamı çerçeve bağımsız düz PHP ve dışarıdan tek paket istemiyor:
<?php
declare(strict_types=1);
final class ServisBulunamadi extends RuntimeException {}
final class DairesselBagimlilik extends RuntimeException {}
final class Container
{
/** @var array<string, callable> Elle tanimlanan fabrikalar */
private array $factories = [];
/** @var array<string, object> Kurulmus ornekler */
private array $instances = [];
/** @var array<string, true> Su an kurulmakta olanlar */
private array $building = [];
public function set(string $id, callable $factory): void
{
$this->factories[$id] = $factory;
}
public function get(string $id): object
{
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
if (isset($this->building[$id])) {
throw new DairesselBagimlilik(
'Dairesel bagimlilik: ' . implode(' -> ', array_keys($this->building)) . ' -> ' . $id
);
}
$this->building[$id] = true;
try {
$object = isset($this->factories[$id])
? ($this->factories[$id])($this)
: $this->autowire($id);
} finally {
unset($this->building[$id]);
}
return $this->instances[$id] = $object;
}
/** Tip ipuclarini okuyup kurucuyu kendisi doldurur. */
private function autowire(string $id): object
{
if (!class_exists($id)) {
throw new ServisBulunamadi("Tanimsiz servis: {$id}");
}
$class = new ReflectionClass($id);
if (!$class->isInstantiable()) {
throw new ServisBulunamadi("Ornegi alinamaz (soyut veya arayuz): {$id}");
}
$constructor = $class->getConstructor();
if ($constructor === null) {
return new $id();
}
$args = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$args[] = $this->get($type->getName());
continue;
}
if ($parameter->isDefaultValueAvailable()) {
$args[] = $parameter->getDefaultValue();
continue;
}
throw new ServisBulunamadi(
"{$id}::__construct() icindeki \${$parameter->getName()} cozulemedi: "
. 'sinif tipi yok, varsayilan deger yok'
);
}
return $class->newInstanceArgs($args);
}
}
Mekanizma üç satırda bitiyor: ReflectionClass::getConstructor() ile kurucuyu al, getParameters() ile parametreleri gez, her parametrenin ReflectionNamedType tipi yerleşik (builtin) değilse o sınıfı container’dan iste. Özyineleme matruşkanın kendisi: en içteki bebeğe kadar iniyor, oradan geri dönüyor.
$building dizisi ise emniyet kemeri. İçinde zaten kurulmakta olan bir kimlik yeniden istenirse, sonsuz özyineleme yerine okunabilir bir hata atıyor. Bunun neye benzediğini biraz sonra göreceğiz.
Yansımanın faturası gerçekte ne kadar?
Yansıma pahalı diye biliriz. Ölçmeden konuşmak yok. Dört sınıflık bir grafiği 20.000 kez kurdum: bir kez yansımayla, bir kez de elle yazılmış fabrika koduyla (derlenmiş container’ın ürettiği şeyin kaba karşılığı).
| Kurulum yolu | 20.000 istek | İstek başına |
|---|---|---|
| Yansımalı autowiring (4 sınıf) | 62–66 ms | 0,0032 ms |
| Elle yazılmış fabrika (4 sınıf) | 7,6–16,2 ms | 0,0004 ms |
| Yansımalı autowiring (120 sınıf) | 158–196 ms / 2.000 istek | 0,079–0,098 ms |
Oran sekiz kata yakın. Mutlak sayı ise istek başına üç mikrosaniye. Yüz yirmi sınıflık bir grafikte bile bir milisaniyenin onda birini geçmiyor.
Sonuç: Yansıma sizin darboğazınız değil. Bir WHERE koşulunun sırası ya da tek bir N+1 döngüsü burada yazan sayının bin katını yiyor. Container’ı derlemek, uygulama yüzlerce servisi her istekte gerçekten kuruyorsa anlamlı; onun öncesinde erken optimizasyon.
Autowiring’in çözemediği üç şey
Tip ipucu okumak, niyet okumak değil. Yukarıdaki container üç yerde duruyor ve bu üç hata mesajı gerçek çıktıdır, elle yazılmış süs değil:
$c = new Container();
// 1) Sepet -> Fiyatlandirma -> Sepet
$c->get(Sepet::class);
// DairesselBagimlilik: Dairesel bagimlilik: Sepet -> Fiyatlandirma -> Sepet
// 2) Odeme::__construct(private string $apiKey)
$c->get(Odeme::class);
// ServisBulunamadi: Odeme::__construct() icindeki $apiKey cozulemedi:
// sinif tipi yok, varsayilan deger yok
// 3) Kargo::__construct(private Tasiyici $tasiyici) — Tasiyici bir arayüz
$c->get(Kargo::class);
// ServisBulunamadi: Tanimsiz servis: Tasiyici
// Üçüncüsünün çözümü tek satır: arayüzü somut sınıfa bağla
$c->set(Tasiyici::class, static fn (Container $c): Tasiyici
=> new Yurtici(getenv('KARGO_API_KEY') ?: 'test-key'));
echo $c->get(Kargo::class)->gonder('1042'); // YK-1042
- Dairesel bağımlılık: İki sınıf birbirini kurucuda istiyorsa container çaresiz. Çözüm container’da değil tasarımda: birini setter ile ver, ya da ortak işi üçüncü bir sınıfa taşı. (Tembel nesnelere geçtiğimizde bu hata kayboluyor; neden kaybolmasına güvenmemek gerektiğini aşağıda yazdım.)
- Skaler parametre:
string $apiKey’in değerini yansımadan okuyamazsınız. Bu bilgi yapılandırmada durur; container’a elle tanımlanır. - Arayüz: Autowiring’in en çok yanlış anlaşılan sınırı.
Tasiyicibir arayüzse container hangi somut sınıfı kuracağını bilemez. Kararı siz verirsiniz — ki bu zaten iyi bir şey.
Yani container kodun yüzde doksanını otomatik kurar, kalan yüzde onu siz yazarsınız. O yüzde on de uygulamanın gerçek kararlarının durduğu yerdir.
PHP 8.4’ten beri tembellik motorun içinde
Giriştekinin cevabı burada. Yedi nesnenin beşi boşunaydı, çünkü container hepsini kurucuya girerken istekli (eager) kuruyor. PdfRenderer o istekte hiç çalıştırılmayacaktı ama yine de kuruldu.
Eskiden bunun çözümü vekil üreten bir kütüphaneydi. PHP 8.4 ile gerek kalmadı: lazy objects (tembel nesneler) artık motorun kendi işi. Arnaud Le Blanc ve Nicolas Grekas’ın yazdığı RFC 26’ya 5 oyla kabul edildi ve PHP 8.4 ile geldi.
- Tembel hayalet (ghost): Nesne kendi içinde, yerinde kuruluyor. Kurulduktan sonra normal nesneden ayırt edilemez.
- Tembel vekil (proxy): Fabrika gerçek örneği döndürüyor, vekil her şeyi ona yönlendiriyor. Kurulumu başka bir tarafa (container’a) devrediyorsanız bu lazım.
Container’da işimize yarayan ikincisi. Bağımlılık olarak verilecek her servisi gerçek nesne yerine vekil olarak veriyoruz:
// Container::autowire() içinde tek satır değişiyor:
// $args[] = $this->get($type->getName());
// yerine:
$args[] = $this->tembelAl($type->getName());
// ...ve sınıfa şu metot ekleniyor:
/** Bağımlılık olarak verilecek servisi tembel vekil olarak döndürür. */
private function tembelAl(string $id): object
{
if (!class_exists($id)) {
return $this->get($id); // hatayı get() atsın
}
$class = new ReflectionClass($id);
$proxy = $class->newLazyProxy(fn (): object => $this->get($id));
// Tembel işaretlenecek özelliği olmayan sınıf tembel olamaz: gerçeğini ver.
return $class->isUninitializedLazyObject($proxy) ? $proxy : $this->get($id);
}
Aynı grafiği aynı makinede iki container ile kurdum:
| Container | Kurulan nesne | Süre | Kurulanlar |
|---|---|---|---|
| İstekli | 7 | 50,5 ms | Config, Logger, Database, PdfRenderer, MailQueue, ReportService, ReportController |
| Tembel vekilli | 4 | 30,3 ms | ReportController, ReportService, Database, Config |
Dürüst olalım: Milisaniye farkı benim kurgumun rakamı. Örnekteki Database ve PdfRenderer kurucularına bağlantı ve font yükleme maliyetini temsil etsin diye 30 ve 20 milisaniyelik bekleme koydum; kazanç tam olarak hiç çalışmayan PdfRenderer kadar. Taşınabilir olan sayı milisaniye değil, yedi yerine dört. Sizin milisaniyeniz, kurulmayan o nesnelerin gerçekte ne kadar sürdüğü kadar olacak.
Bir ayrıntı da şu: Logger listede yok, çünkü onu isteyen kod yalnızca bir metodunu çağırdı ve o metot hiçbir özelliğe dokunmadı. Vekili uyandıran şey metot çağrısı değil, durumun okunması veya yazılması.
Gizli hazine: dairesel bağımlılık çözülüyor
Tembel vekile geçince beklemediğim bir şey oldu. Biraz önce container’ı durduran Sepet → Fiyatlandirma → Sepet döngüsü artık kuruluyor, çünkü Sepet kurulurken eline geçen Fiyatlandirma henüz kurulmamış bir vekil. İki yönü de deneyip doğruladım:
// Sepet -> Fiyatlandirma -> Sepet ; tembel container ile:
$sepet = $c->get(Sepet::class);
$sepet->ekle('klavye', 2);
$sepet->ekle('fare', 1);
echo $sepet->toplam(); // 360
echo $c->get(Fiyatlandirma::class)->sepetSatirSayisi(); // 2
İki klavye bir fare, yüzde 20 KDV ile 360 çıkıyor; ters yönde de aynı sepetin iki satırı görünüyor. Tek nesne, iki yön, çalışıyor.
Ama: bu bir çözüm değil, bir örtü. Döngü hâlâ orada ve ilk dokunuşa kadar sessiz bekliyor. Kurucuda karşılıklı bağımlılık varsa tasarımı düzeltin; tembel vekil size sadece sabah yedide çöken bir hatayı akşam yediye erteleme imkânı verir.
Tembel nesnenin iki tuzağı
Birincisi: özelliği olmayan bir sınıf tembel olamıyor. Dokümanın dediği gibi, tembel işaretlenecek özellik kalmadığında nesne tembel sayılmıyor. isUninitializedLazyObject() böyle bir sınıfta false dönüyor; yukarıdaki kodda o yüzden bir kontrol var. Kontrolü koymazsanız, tembel sandığınız ama aslında kurulmuş bir nesneyi vekil olarak dolaştırırsınız.
İkincisi: dahili sınıflar hiç olmuyor. ArrayObject, PDO, DateTime — hepsi kapının dışında:
// Lazy proxy ile dahili sinif denemesi
$r = new ReflectionClass(ArrayObject::class);
$r->newLazyProxy(fn (): ArrayObject => new ArrayObject());
// Error: Cannot make instance of internal class lazy: ArrayObject is internal
Pratikte bu, pahalı bağlantıyı doğrudan PDO üzerinden değil, kendi sarmalayıcı sınıfınız üzerinden tembelleştirmeniz gerektiği anlamına geliyor. Zaten yapmanız gereken şey.
Elinizdeki PHP’nin bunu destekleyip desteklemediğini tek satırda görürsünüz (sürüm yükseltmenin güncesini ayrıca yazmıştım):
php -r 'var_dump(method_exists(ReflectionClass::class, "newLazyProxy"));'
PSR-11 ve paspasın altındaki anahtar
Container yazarken uyacağınız standart PSR-11. Beklenenden küçük: Psr\Container\ContainerInterface yalnızca iki metot istiyor — get(string $id) ve has(string $id): bool. Yanında iki istisna arayüzü var: ContainerExceptionInterface ve onu genişleten NotFoundExceptionInterface. Kural net: has() false dönüyorsa get() NotFoundExceptionInterface atmak zorunda.
Standardın kendi metninde bir uyarı da var ve asıl kıymetli kısım orası: bir nesneye, kendi bağımlılıklarını içinden çekebilsin diye container verilmemeli. Bunun adı Service Locator ve PSR-11 bunu açıkça bir anti-desen olarak anıyor.
Matruşka diliyle: container bebeği size uzatan eldir, elinizi içine soktuğunuz çekmece değil. Sınıfınız kurucusunda Database isterse bağımlılığı okunur. Kurucusunda container isteyip içinden get('db') çekerse, bağımlılık listesi gövdenin içine saklanmış olur. Kapıyı kilitleyip anahtarı paspasın altına koymak gibi.
Hangi durumda ne kullanmalı?
| Durum | Öneri | Gerekçe |
|---|---|---|
| 5–10 sınıf, tek giriş noktası | Container kullanmayın | Yedi satırlık elle kurulum daha okunur; soyutlama maliyeti kazancından büyük |
| Birkaç giriş noktası, kendi kodunuz | Bu yazıdaki gibi kendi container’ınız | Yüz satır, sıfır bağımlılık, hata mesajları sizin dilinizde |
| Çok paket, çok ekip, kurulum kuralları karmaşık | Hazır container | PHP-DI, league/container ve Symfony DependencyInjection’ın üçü de MIT lisanslı; tanım dosyaları, etiketler ve dekoratörler hazır geliyor |
| Her istekte yüzlerce servis gerçekten kuruluyor | Derlenmiş container | Yansımayı istek anından çıkarıp üretilmiş fabrika koduna taşır |
| Nesne kurulumu pahalı (bağlantı, font, büyük dosya) | Tembel vekil | Kurulmayan nesnenin maliyeti sıfır; PHP 8.4’ten beri kütüphanesiz |
Çerçeve kullandığım projelerde bu kararların çoğu zaten verilmiş oluyor; çerçevenin container’ı orada duruyor. Çerçevesiz yazdığım işlerde ise yukarıdaki sınıf, kopyalayıp üzerine bir iki satır eklediğim bir başlangıç noktası.
Matruşkayı açmadan önce
Matruşkanın güzelliği dışarıdan tek bebek görünmesinde. Container’ın tehlikesi de tam orada: get() tek satır, arkasında kaç nesne kurulduğunu söylemiyor. Giriştekini hatırlayın — yedi kuruldu, iki kullanıldı, kimse fark etmedi.
Üç şey yapın. Kurduğunuz nesneleri bir kez sayın; pahalı olanları tembel yapın; container’ı sınıfın kurucusuna sokmayın.
Geçen yazıda PHP’yi kalıba döken bir derleyiciyi yazmıştım; orada dinamizmden vazgeçmenin bedelini konuşuyorduk. Bu yazı da aynı madalyonun öbür yüzü: çalışma anında tip okuyup nesne kuran yüz satır, kalıba dökülmeyecek kadar dinamik olduğu için işimizi görüyor. PHP’nin pahalı dediğimiz esnekliğinin faturası, ölçtüğümüzde çoğu zaman üç mikrosaniye çıkıyor.