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.
versiyon: 3.x