# PHP DI Container: Bağımlılıkların Matruşkası

- Yazar: [Halit Yeşil](https://halityesil.com/)
- Yayın tarihi: 2026-10-03
- Dil: Türkçe
- Kaynak adres: https://halityesil.com/php-di-container-autowiring/
- İngilizce sürümü: https://halityesil.com/en/php-autowiring-container-lazy-objects.md
- Kategori: PHP
- Etiketler: autowiring, bağımlılık enjeksiyonu, di container, performans, php, PSR-11, reflection, tembel nesne
- Lisans: https://creativecommons.org/licenses/by-nc/4.0/
- Atıf: Halit Yeşil. “PHP DI Container: Bağımlılıkların Matruşkası”. halityesil.com, 2026-10-03. https://halityesil.com/php-di-container-autowiring/

---

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](https://halityesil.com/php/bagimlilik-enjeksiyonu-kodunuzda-esnekligi-saglayan-guclu-bir-tasarim-ilkesi/); 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?

```php
// 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
<?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ığı).

<figure class="wp-block-table"><table><thead><tr><th>Kurulum yolu</th><th>20.000 istek</th><th>İstek başına</th></tr></thead><tbody><tr><td>Yansımalı autowiring (4 sınıf)</td><td>62–66 ms</td><td>0,0032 ms</td></tr><tr><td>Elle yazılmış fabrika (4 sınıf)</td><td>7,6–16,2 ms</td><td>0,0004 ms</td></tr><tr><td>Yansımalı autowiring (120 sınıf)</td><td>158–196 ms / 2.000 istek</td><td>0,079–0,098 ms</td></tr></tbody></table>

<figcaption class="wp-element-caption">PHP 8.4.21 CLI, tek süreç, önbellek yok; her istekte container sıfırdan kuruluyor.</figcaption></figure>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ı](https://halityesil.com/php/mysql/mysql-where-kosullari-sirasi-1-milyon-satirda-100-kat-performans-farki/) ya da [tek bir N+1 döngüsü](https://halityesil.com/php/mysql/mysqlde-kral-katili-n1-sorgu-problemini-cozmek/) 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:

```php
$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ı. `Tasiyici` bir 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](https://www.php.net/manual/en/language.oop5.lazy-objects.php)* (tembel nesneler) artık motorun kendi işi. Arnaud Le Blanc ve Nicolas Grekas’ın [yazdığı RFC](https://wiki.php.net/rfc/lazy-objects) 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:

```php
// 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:

<figure class="wp-block-table"><table><thead><tr><th>Container</th><th>Kurulan nesne</th><th>Süre</th><th>Kurulanlar</th></tr></thead><tbody><tr><td>İstekli</td><td>7</td><td>50,5 ms</td><td>Config, Logger, Database, PdfRenderer, MailQueue, ReportService, ReportController</td></tr><tr><td>Tembel vekilli</td><td>4</td><td>30,3 ms</td><td>ReportController, ReportService, Database, Config</td></tr></tbody></table>

<figcaption class="wp-element-caption">Üç ayrı koşuda aynı sayılar; sipariş listesi iki kaydı her iki yolda da doğru döndü.</figcaption></figure>**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:

```php
// 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:

```php
// 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](https://halityesil.com/php/php-surum-yukseltme-8-1-den-8-5-e/) ayrıca yazmıştım):

```bash
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**](https://www.php-fig.org/psr/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ı?

<figure class="wp-block-table"><table><thead><tr><th>Durum</th><th>Öneri</th><th>Gerekçe</th></tr></thead><tbody><tr><td>5–10 sınıf, tek giriş noktası</td><td>Container kullanmayın</td><td>Yedi satırlık elle kurulum daha okunur; soyutlama maliyeti kazancından büyük</td></tr><tr><td>Birkaç giriş noktası, kendi kodunuz</td><td>Bu yazıdaki gibi kendi container’ınız</td><td>Yüz satır, sıfır bağımlılık, hata mesajları sizin dilinizde</td></tr><tr><td>Çok paket, çok ekip, kurulum kuralları karmaşık</td><td>Hazır container</td><td>PHP-DI, league/container ve Symfony DependencyInjection’ın üçü de MIT lisanslı; tanım dosyaları, etiketler ve dekoratörler hazır geliyor</td></tr><tr><td>Her istekte yüzlerce servis gerçekten kuruluyor</td><td>Derlenmiş container</td><td>Yansımayı istek anından çıkarıp üretilmiş fabrika koduna taşır</td></tr><tr><td>Nesne kurulumu pahalı (bağlantı, font, büyük dosya)</td><td>Tembel vekil</td><td>Kurulmayan nesnenin maliyeti sıfır; PHP 8.4’ten beri kütüphanesiz</td></tr></tbody></table>

<figcaption class="wp-element-caption">Lisanslar 1 Ekim 2026 itibarıyla projelerin GitHub depolarından doğrulandı.</figcaption></figure>Ç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](https://halityesil.com/php/typephp-php-aot-derleyici/) 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.
