# 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.

- Yayın: 2021-11-07
- Kategori: Diller
- Etiketler: PHP
- Okuma süresi: 4 dk okuma
- Kaynak: https://www.muhammetsafak.com.tr/blog/php-8-1-yolda-enumlar-ve-readonly-ile-daha-guvenli-modeller/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
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](/blog/php-8-0-a-hazirlik-union-types-match-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](/blog/phpde-enum-ihtiyaci-8-1-oncesi-cozumler) 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
<?php

enum OrderStatus
{
    case Pending;
    case Processing;
    case Shipped;
    case Cancelled;
}
```

Artık bir fonksiyon imzasına `OrderStatus` yazabilirsiniz:

```php
<?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
<?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
<?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
<?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
<?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
<?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
<?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.
