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. Martin Fowler tanımı iki cümleye sığdırıyor: özelliklerinin değeri sayesinde eşit olan nesnelere değer nesnesi denir ve “değer nesneleri değişmez olmalıdır”.
İ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;
}
}
Doğrulamayı FILTER_VALIDATE_EMAIL yapıyor; 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 — kılavuzun tarif ettiği gibi readonly bir özellik yalnız tanımlandığı sınıfın kapsamından bir kez atanabiliyor, sonrasında değiştirilemiyor:
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ı; Fowler’ın ayrımıyla referans nesnelerinde önemli olan referansın kendisi, değer nesnelerinde ise içeriğin değeri.
$email1 = new Email('test@example.com');
$email2 = new Email('TEST@EXAMPLE.COM');
$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.
Kaynaklar
Bu konuda sorulanlar
readonly value object'lerimde "withX" tarzı kopyalama metotlarını nasıl temiz yazarım?
`readonly`'de withX yeni instance döndürür: taşınabilir yol `new self` + named arguments, PHP 8.5'te doğrulama basitse `clone($this, [...])`.
Integer cent olarak sakladığım para alanları için custom cast mı yoksa accessor/mutator mı kullanmalıyım?
DB'de integer cent, modelde immutable `Money`: eşlemeyi tek bir `CastsAttributes` sınıfına toplayın, currency'yi aynı cast'ta ikinci kolon yapın.
Bu konudaki deneyler
Framework'e bağlanmadan çalışan modüler PHP kütüphaneleri; 39 paketlik InitPHP ekosistemine ve Framework3'e dönüştü.
Şu an ne yapıyor
39 paketlik bir PHP kütüphane ailesi: Router, Database, Cache, Mailer, Socket ve diğerleri hem tek başına hem birlikte çalışıyor, hepsi Composer/Packagist üzerinden MIT lisansıyla dağıtılıyor. Framework'e bağlanmadan parça kullanmak isteyen PHP geliştiricisi bugün kurabilir.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.