PHP 8.1 yolda: enum'lar ve readonly ile daha güvenli modeller
PHP 8.1 henüz çıkmadı; ama gelen enum ve readonly özelliklerinin domain modellemesine nasıl etki edeceğini inceliyorum.
PHP 8.1 aralık ayına doğru çıkması bekleniyor. Release candidate’leri takip ediyorum ve özellikle iki özellik dikkatimi çekiyor: native enum desteği ve readonly properties. Bu ikisi, dil düzeyinde daha güvenli domain modelleri yazma kapısını açıyor. Henüz production’da kullanamıyorum, ama sürüm çıkmadan bu özelliklerin ne getireceğini somut olarak düşünmek istiyorum.
Neden bu iki özellik önemli
PHP 8.0 union types, match expression ve named arguments gibi anlamlı eklemeler getirdi. 8.1 ise daha çok domain modelleme katmanını etkiliyor.
Bir domain modelinde iki yaygın sorun var:
Birincisi, bir alanın alabileceği değerlerin serbest string veya int olarak temsil edilmesi. Sipariş durumu, kullanıcı rolü, ödeme tipi — bunlar kapalı kümeler. PHP bugüne kadar bu kümeleri sınıf sabitleri veya paket kütüphaneleriyle temsil etmek zorundaydı.
İkincisi, bir kez oluşturulan nesnenin sonradan değiştirilmemesi gereken alanlarını koruyamamak. Value object’leri immutable tutmak için constructor sonrası setter yazmamak ya da __set ile engellemek gerekiyordu; her ikisi de zahmetli.
Native enum: dil bir özelliği düzeyinde kapalı kümeler
PHP 8.1 ile gelen native enum, sınıf sabitlerinden çok farklı. Gerçek bir tip oluşturuyor:
<?php
enum OrderStatus
{
case Pending;
case Processing;
case Shipped;
case Cancelled;
}
Artık bir fonksiyon imzasına OrderStatus yazabilirsiniz:
<?php
function processOrder(Order $order, OrderStatus $status): void
{
$order->status = $status;
}
processOrder($order, OrderStatus::Processing);
processOrder($order, 'processing'); // TypeError — derleme değil, çalışma anında
Yanlış bir değer geçirilmesi mümkün değil; tip sistemi bunu engelliyor.
Backed enum: Veritabanında veya JSON’da string/int değer saklamak için backed enum kullanılıyor:
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
// Veritabanından dönüştürme:
$status = OrderStatus::from('pending'); // OrderStatus::Pending
$status = OrderStatus::tryFrom('bilinmeyen'); // null, istisna fırlatmaz
Enum metodları: Enum’lar metot da içerebiliyor:
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Processing = 'processing';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
OrderStatus::Pending => 'Beklemede',
OrderStatus::Processing => 'İşleniyor',
OrderStatus::Shipped => 'Kargoya verildi',
OrderStatus::Cancelled => 'İptal edildi',
};
}
public function isFinal(): bool
{
return match($this) {
OrderStatus::Shipped, OrderStatus::Cancelled => true,
default => false,
};
}
}
Bu, daha önce OrderStatus sınıfı içinde manuel yazdığım davranışları artık enum içinde doğal olarak barındırabilmek demek.
tryFrom ile güvenli dönüştürme
Veritabanından okunan değerlerin her zaman geçerli bir enum değeri olacağını garanti edemezsiniz; tabloya doğrudan veri girmek mümkün, eski migration’lardan kalan değer olabilir. Bu yüzden from() yerine tryFrom() tercih etmek daha güvenli bir alışkanlık:
<?php
$raw = $row['status']; // Veritabanından gelen ham değer
$status = OrderStatus::tryFrom($raw);
if ($status === null) {
// Bilinmeyen değer; loglayıp varsayılanla devam etmek ya da hata fırlatmak
throw new \UnexpectedValueException("Bilinmeyen sipariş durumu: {$raw}");
}
from() bilinmeyen değerde ValueError fırlatır; tryFrom() ise null döndürür. Hangisini kullanacağınız, “bu durumun oluşması bir hata mı yoksa beklenen bir durum mu?” sorusuna bağlı.
Readonly özellikler: değişmezliği dil düzeyinde
Bir value object oluşturulduktan sonra değişmemeli. Bugün bunu zorlamak için ya her özelliği private yapıp getter yazıyorsunuz ya da başka hileler kullanıyorsunuz:
<?php
// Bugün: her özellik için getter yazmak
class Money
{
private int $amount;
private string $currency;
public function __construct(int $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
public function getAmount(): int { return $this->amount; }
public function getCurrency(): string { return $this->currency; }
}
PHP 8.1 ile readonly özellik:
<?php
class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
}
$money = new Money(100, 'TRY');
echo $money->amount; // 100 — public, okunabilir
$money->amount = 200; // Error: Cannot modify readonly property
public readonly demek, “herkes okuyabilir ama yalnızca constructor atayabilir” demek. Getter yazmadan immutable değer nesnesi kuruluyor.
Bu ikisini birleştirmek
Enum ve readonly birlikte domain modeli çok daha sağlam hale getiriyor:
<?php
class Order
{
public function __construct(
public readonly int $id,
public readonly int $userId,
public OrderStatus $status,
public readonly \DateTimeImmutable $createdAt,
) {}
}
$order = new Order(
id: 1,
userId: 42,
status: OrderStatus::Pending,
createdAt: new \DateTimeImmutable(),
);
$order->status = OrderStatus::Processing; // Geçerli — status readonly değil
$order->id = 99; // Error — id readonly
Beklenti değerlendirmesi
PHP 8.1’in bu iki özelliği, dil düzeyinde daha güvenli modeller kurmayı sağlıyor. Daha önce paket veya manuel yöntemlerle çözdüğümüz şeyleri artık native olarak yapabilmek, hem bağımlılığı azaltıyor hem de dil uyumluluğunu güçlendiriyor.
Sürüm çıktığında geçişi aşamalı yapmayı planlıyorum: önce yeni yazılan modellerde, sonra kritik domain nesnelerinde. Enum için mevcut sınıf sabitlerini döndüren tüm yerleri gözden geçirmek gerekecek — her sabiti enum’a çevirmek anlamsız, ama durumu temsil eden kapalı kümeleri native enum’a taşımak mantıklı. Readonly için ise değer nesnesi olarak kullandığım tüm sınıflar öncelikli adaylar.
Enum’ların interface implement edebilmesi de dikkat çekici bir özellik. Bir bildirim sisteminde tüm bildirim tiplerinin ortak bir davranışı paylaşması gerekiyorsa, enum bunu interface üzerinden garanti altına alabiliyor. Sınıf hiyerarşisi kurmadan, kapalı bir küme içindeki her elemanın aynı sözleşmeye uyduğunu dil düzeyinde belirtmek: bu, PHP’nin tip sisteminin olgunlaştığının net bir göstergesi.