Modern PHP'de value object tasarlamak
İlkel tipler yerine anlamı koda gömmek: PHP'de value object tasarımının neden ve nasılı üzerine.
Bir kullanıcının e-posta adresini string olarak saklıyoruz. Sipariş tutarını float olarak tutuyoruz. Koordinatları iki ayrı float olarak taşıyoruz. Bu yaklaşım işe yarıyor; ama bir maliyeti var: o string’in gerçekte ne anlama geldiğini, hangi kurallara tabi olduğunu, nerede geçerli ya da geçersiz sayıldığını kodun kendisi bilmiyor.
Value object, bu anlam kaybının çözümü. Kavram DDD (Domain-Driven Design — Alan Odaklı Tasarım) literatüründen geliyor ama pratikte herhangi bir projede uygulanabilir. Özü basit: bir kavramı temsil eden, kimliği değil değeri önemli olan, değişmez (immutable) nesneler.
İlkel takıntısı (primitive obsession) neden sorun
Kodda ilkel tip kullanımı çoğaldığında birkaç şey kaçınılmaz oluyor:
Doğrulama mantığı çoğalıyor ve dağılıyor. E-posta doğrulama bir controller’da, bir form request’te, belki bir model event’ında yazılıyor. Hangisi doğru? Hepsi? Hiçbiri?
Tip sistemi size yardım edemiyor. createOrder(string $email, float $amount) imzasına bakarak bu iki string’in birbirinin yerine geçip geçemeyeceğini anlayamazsınız. Derleme zamanında da, IDE’de de.
Kural değiştiğinde (e-posta artık farklı bir kurala tabi) tüm kullanım yerlerini bulmak gerekiyor. Gözden kaçan her yer bir hata.
Basit bir value object: Email
final class Email
{
private string $value;
public function __construct(string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException(
"Geçersiz e-posta adresi: {$value}"
);
}
$this->value = strtolower($value);
}
public function value(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}
Bu nesne oluşturulamadan hatalı bir e-posta sisteme giremez. Doğrulama tek yerde yaşıyor. Email alan bir fonksiyon imzası, o parametrenin ne olduğunu açıkça söylüyor.
PHP 8.1 readonly ile immutability’yi güçlendirmek
PHP 8.1 ile gelen readonly özelliği value object’leri daha temiz yazmanızı sağlıyor:
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Para miktarı negatif olamaz.');
}
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException('Para birimi 3 harfli ISO kodu olmalıdır.');
}
}
public function add(self $other): self
{
if ($this->currency !== $other->currency) {
throw new \LogicException('Farklı para birimleri toplanamaz.');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(self $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
Money burada immutable. add() yeni bir nesne döndürüyor, mevcut nesneyi değiştirmiyor. Bu value object’lerin temel kuralı: bir kez oluşturulduktan sonra içi değişmez, yeni bir değer istiyorsanız yeni bir nesne oluşturursunuz.
Eşitlik kimlikle değil değerle ölçülür
Değer nesnelerinin ayırt edici özelliği eşitliğin nasıl tanımlandığı. İki farklı Email nesnesi aynı e-posta adresini taşıyorsa eşittir — hangi nesne referansına sahip olduğunuz önemli değil. Bu, varlıkların (entity) kimliğe göre eşitlik kurmasından temel olarak farklı.
$email1 = new Email('[email protected]');
$email2 = new Email('[email protected]');
$email1->equals($email2); // true — her ikisi de normalleştirildi
Value object’leri ne zaman kullanmalı
Her ilkel tipi sarmak gerekmez. Şu sorular yol gösterici:
- Bu değerin belirli bir geçerlilik kuralı var mı?
- Bu değer için iş mantığı (business logic) yazılması gerekiyor mu?
- Kodun farklı yerlerinde aynı doğrulama tekrarlanıyor mu?
- Bu tipin yanlış yere geçirilmesi derleyici veya IDE tarafından yakalanabiliyor mu?
Bu sorulardan birine bile “evet” yanıtı verildiyse value object muhtemelen değer yaratacak.
Koordinatlar, para miktarı, e-posta, telefon numarası, renk kodu, URL — bunların hepsi value object adayı. Öte yandan kullanıcının adı veya bir sayfa başlığı gibi yalnızca string olan ve iş kuralı taşımayan alanlar için bu yapıya gerek yok.
Sonuç
Value object’ler kodu uzatır ama anlam kaybını önler. Bir metot imzasına bakıp o metodun ne aldığını, hangi kurallar altında çalıştığını anlamak, ilkel tip yığınına kıyasla çok daha hızlı. Bu hız birikimi uzun soluklu projelerde ciddi bir bakım kolaylığına dönüşüyor.
Modern PHP — readonly property’ler, constructor promotion, isimlendirme yetkinliği — bu kalıbı yazmayı her zamankinden kolay hale getiriyor. Gerekçe zaten yıllardır yerinde; araçlar da artık yeterli.