Bağımlılıkların Aktarılması
***************************
<div class=perex>
Argümanlar, ya da DI terminolojisiyle "bağımlılıklar", sınıflara başlıca şu yollarla aktarılabilir:
* Yapıcı enjeksiyonu (constructor injection)
* Metot enjeksiyonu (setter injection denir)
* Özellik enjeksiyonu (property injection)
* `inject*()` metodu ya da `#[Inject]` attribute'u ile
</div>
Her seçeneği somut örneklerle gösterelim.
Yapıcı Enjeksiyonu
==================
Bağımlılıklar, nesne örneklenirken yapıcı argümanları olarak verilir:
```php
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
```
Bu yaklaşım, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur; çünkü onlar olmadan örnek oluşturulamaz.
PHP 8.0'dan beri, işlevsel olarak eşdeğer olan daha kısa bir yazım ([constructor property promotion |https://blog.nette.org/tr/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]) kullanabiliriz:
```php
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
```
PHP 8.1'den beri bir özellik `readonly` bayrağıyla işaretlenebilir; bu, özelliğin değerinin başlatmadan sonra değişmeyeceğini bildirir:
```php
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
```
DI container, bağımlılıkları yapıcıya [autowiring |autowiring] ile otomatik olarak aktarır. Bu yolla verilemeyen argümanlar (örneğin dizeler, sayılar, boolean değerler) [yapılandırmada belirtilir |services#Argümanlar].
Constructor Hell
----------------
*Constructor hell* terimi, bir alt sınıfın, yapıcısı bağımlılık isteyen bir üst sınıftan türediği ve alt sınıfın da kendi bağımlılıklarını istediği durumu anlatır. O zaman üst sınıfın bağımlılıklarını da alıp aktarması gerekir:
```php
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
```
Sorun, `BaseClass` sınıfının yapıcısını değiştirmek istediğimizde, örneğin yeni bir bağımlılık eklendiğinde ortaya çıkar. O zaman alt sınıfların tüm yapıcılarını da değiştirmek gerekir. Bu da böyle bir değişikliği cehenneme çevirir.
Bu nasıl önlenir? Çözüm, **[kompozisyonu kalıtıma yeğlemektir |faq#Kompozisyon neden kalıtıma yeğlenir?]**.
Yani kodu farklı tasarlarız. [Soyut |nette:introduction-to-object-oriented-programming#Soyut Sınıflar] `Base*` sınıflarından kaçınacağız. `MyClass`, belirli bir işlevi `BaseClass` sınıfından türeyerek edinmek yerine, bu işlevi bağımlılık olarak alacak:
```php
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
```
Setter Enjeksiyonu
==================
Bağımlılıklar, onları private bir özellikte saklayan bir metot çağrılarak verilir. Bu metotların yaygın adlandırma uzlaşımı `set*()` biçimidir, bu yüzden onlara setter denir; ama elbette başka türlü de adlandırılabilirler.
```php
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
```
Bu yaklaşım, sınıfın çalışması için zorunlu olmayan isteğe bağlı bağımlılıklar için uygundur; çünkü nesnenin bağımlılığı gerçekten alacağı (yani çağıranın metodu çağıracağı) garanti değildir.
Aynı zamanda bu yöntem, bağımlılığı değiştirmek için setter'ın defalarca çağrılmasına olanak tanır. Bu istenmiyorsa metoda bir denetim ekleyin ya da PHP 8.1'den beri `$cache` özelliğini `readonly` bayrağıyla işaretleyin.
```php
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
```
Setter çağrısı, DI container yapılandırmasında [setup anahtarında |services#Setup] tanımlanır. Burada da bağımlılıkların autowiring ile otomatik aktarımı kullanılır:
```neon
services:
- create: MyClass
setup:
- setCache
```
Özellik Enjeksiyonu
===================
Bağımlılıklar, doğrudan bir üye özelliğe yazılarak verilir:
```php
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
```
Bu yöntem uygunsuz sayılır; çünkü üye özelliğin `public` olarak bildirilmesi gerekir. Sonuç olarak, aktarılan bağımlılığın gerçekten gereken türde olduğunu güvence altına alma denetimini yitiririz (bu özellikle PHP 7.4'ün özellik tür bildirimlerinden önce geçerliydi) ve yeni atanan bir bağımlılığa özel mantıkla tepki verme, örneğin sonradan değiştirilmesini önleme olanağını kaybederiz. Aynı zamanda özellik, sınıfın public API'sinin bir parçası hâline gelir; bu da istenmeyebilir.
Özellik ataması, DI container yapılandırmasında [setup bölümünde |services#Setup] tanımlanır:
```neon
services:
- create: MyClass
setup:
- $cache = @\Cache
```
Inject
======
Önceki üç yaklaşım tüm nesne yönelimli dillerde genel olarak geçerliyken, `inject*()` metotlarıyla ya da `#[Inject]` attribute'uyla enjeksiyon tipik olarak Nette presenter'larında kullanılır ve orada varsayılan olarak etkindir; başka herhangi bir servis [`inject: true` |services#Inject Kipi] ile bunu açabilir. Bunlar [ayrı bir bölümde |best-practices:inject-method-attribute] ele alınıyor.
Hangi Yöntemi Seçmeli?
======================
- Yapıcı, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur.
- Setter ise tersine, isteğe bağlı ya da sonradan değiştirilmesi gerekebilecek bağımlılıklar için uygundur.
- Public özellikler genellikle önerilmez.
Bağımlılıkların Aktarılması
Argümanlar, ya da DI terminolojisiyle “bağımlılıklar”, sınıflara başlıca şu yollarla aktarılabilir:
- Yapıcı enjeksiyonu (constructor injection)
- Metot enjeksiyonu (setter injection denir)
- Özellik enjeksiyonu (property injection)
inject*() metodu ya da #[Inject] attribute'u ile
Her seçeneği somut örneklerle gösterelim.
Yapıcı Enjeksiyonu
Bağımlılıklar, nesne örneklenirken yapıcı argümanları olarak verilir:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Bu yaklaşım, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur; çünkü onlar
olmadan örnek oluşturulamaz.
PHP 8.0'dan beri, işlevsel olarak eşdeğer olan daha kısa bir yazım (constructor property
promotion) kullanabiliriz:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
PHP 8.1'den beri bir özellik readonly bayrağıyla işaretlenebilir; bu, özelliğin değerinin başlatmadan
sonra değişmeyeceğini bildirir:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
DI container, bağımlılıkları yapıcıya autowiring
ile otomatik olarak aktarır. Bu yolla verilemeyen argümanlar (örneğin dizeler, sayılar, boolean değerler) yapılandırmada belirtilir.
Constructor Hell
Constructor hell terimi, bir alt sınıfın, yapıcısı bağımlılık isteyen bir üst sınıftan türediği ve alt
sınıfın da kendi bağımlılıklarını istediği durumu anlatır. O zaman üst sınıfın bağımlılıklarını da alıp
aktarması gerekir:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Sorun, BaseClass sınıfının yapıcısını değiştirmek istediğimizde, örneğin yeni bir bağımlılık
eklendiğinde ortaya çıkar. O zaman alt sınıfların tüm yapıcılarını da değiştirmek gerekir. Bu da böyle bir
değişikliği cehenneme çevirir.
Bu nasıl önlenir? Çözüm, kompozisyonu kalıtıma
yeğlemektir.
Yani kodu farklı tasarlarız. Soyut Base*
sınıflarından kaçınacağız. MyClass, belirli bir işlevi BaseClass sınıfından türeyerek
edinmek yerine, bu işlevi bağımlılık olarak alacak:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Setter Enjeksiyonu
Bağımlılıklar, onları private bir özellikte saklayan bir metot çağrılarak verilir. Bu metotların yaygın adlandırma
uzlaşımı set*() biçimidir, bu yüzden onlara setter denir; ama elbette başka türlü de
adlandırılabilirler.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Bu yaklaşım, sınıfın çalışması için zorunlu olmayan isteğe bağlı bağımlılıklar için uygundur; çünkü
nesnenin bağımlılığı gerçekten alacağı (yani çağıranın metodu çağıracağı) garanti değildir.
Aynı zamanda bu yöntem, bağımlılığı değiştirmek için setter'ın defalarca çağrılmasına olanak tanır. Bu
istenmiyorsa metoda bir denetim ekleyin ya da PHP 8.1'den beri $cache özelliğini readonly bayrağıyla
işaretleyin.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if (isset($this->cache)) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
Setter çağrısı, DI container yapılandırmasında setup anahtarında tanımlanır. Burada da
bağımlılıkların autowiring ile otomatik aktarımı kullanılır:
services:
- create: MyClass
setup:
- setCache
Özellik Enjeksiyonu
Bağımlılıklar, doğrudan bir üye özelliğe yazılarak verilir:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Bu yöntem uygunsuz sayılır; çünkü üye özelliğin public olarak bildirilmesi gerekir. Sonuç olarak,
aktarılan bağımlılığın gerçekten gereken türde olduğunu güvence altına alma denetimini yitiririz (bu özellikle PHP
7.4'ün özellik tür bildirimlerinden önce geçerliydi) ve yeni atanan bir bağımlılığa özel mantıkla tepki verme,
örneğin sonradan değiştirilmesini önleme olanağını kaybederiz. Aynı zamanda özellik, sınıfın public API'sinin bir
parçası hâline gelir; bu da istenmeyebilir.
Özellik ataması, DI container yapılandırmasında setup bölümünde tanımlanır:
services:
- create: MyClass
setup:
- $cache = @\Cache
Inject
Önceki üç yaklaşım tüm nesne yönelimli dillerde genel olarak geçerliyken, inject*() metotlarıyla ya da
#[Inject] attribute'uyla enjeksiyon tipik olarak Nette presenter'larında kullanılır ve orada varsayılan olarak
etkindir; başka herhangi bir servis inject: true ile bunu açabilir.
Bunlar ayrı bir bölümde ele alınıyor.
Hangi Yöntemi Seçmeli?
- Yapıcı, sınıfın çalışması için kesinlikle gereken zorunlu bağımlılıklar için uygundur.
- Setter ise tersine, isteğe bağlı ya da sonradan değiştirilmesi gerekebilecek bağımlılıklar için uygundur.
- Public özellikler genellikle önerilmez.