Bugün, PHP altyapılarında daha önce hiç görülmemiş bir özellik sunuyoruz.
Artık Doppar servisleri #[Immutable] olarak işaretlenebiliyor. Bu işaretlendiğinde, framework uygulama başlatılırken bu servisleri yapılandırıyor — yapılandırma dosyalarınızdan, ortam değişkenlerinden, veritabanlarından, ihtiyacınız olan her şeyden — ve daha sonra kullanım öncesinde kalıcı olarak donduruyor. Bu noktadan sonra, bu serviste bir özelliği değiştirmeye yönelik herhangi bir girişim hemen bir istisna fırlatır, uygulamanın hangi yerinde olursa olsun.
Buna Donmuş Servisler diyoruz.
Her PHP Geliştiricisinin Yayınladığı Hata
Her PHP Geliştiricisinin Yayınladığı Hata
Bu muhtemelen karşılaştığınız veya karşılaşacağınız bir senaryo.
Bir PaymentService kaydettiniz ve onu singleton olarak tanımladınız. Ödeme ağ geçidini, vergi oranını ve canlı modda olup olmadığınızı saklıyor. Standart bir kurulum. Bu servis, talep yaşam döngüsünde paylaşılıyor — bu noktası singletonların amacıdır.
Üç ay sonra, bir junior geliştirici bir middleware yazıyor. Belirli bir rota için ağ geçidini geçici olarak değiştirmeleri gerekiyor. Servisi konteynerde buluyor, özelliği atıyor ve geçiyor. Kod incelemesi bunu atlıyor. Testler başarıyla geçiyor — bu yolu kapsamıyor.
// CheckoutMiddleware.php içindeki bir yer
$payment = app(PaymentService::class);
$payment->gateway = 'sandbox'; // sadece test için, bunu daha sonra kaldıracağım
Bunu kaldırmazlar. Artık o middleware’den geçen her talep — üretimde, gerçek müşterilerle — sandbox ağ geçidini kullanıyor. Hiçbir istisna yok. Hiçbir log girişi yok. CheckoutMiddleware.php‘ye işaret eden bir yığın iz yok. Sadece izlenmesi neredeyse imkansız olan yanlış bir davranış.
Bu uydurulmuş bir örnek değil. Bu, her büyük PHP framework’ün bugüne kadar sessizce izin verdiği gerçek, yaygın, felakete yol açan bir hata türüdür.
Her Framework’ün Yanlış Yaptığı Şey
Her Framework’ün Yanlış Yaptığı Şey
Ecosistem durumunu doğrudan ifade edelim.
Laravel — singleton servisleri sonsuza kadar değiştirilebilir nesnelardır. Konteyner, bir servisin yaşam döngüsüne dair “çözüldü” veya “çözülmedi” kavramlarından başka bir şey sunmaz. Hiçbir şey framework içerisinde paylaşılmış servis durumunun herhangi bir noktada mutasyona uğratılmasını engellemiyor.
Symfony — derlenmiş konteyner dondurulabilir, ancak bu sadece konteynerin bağlantılarını ve meta verilerini dondurur, servis nesnelerini değil. Dondurulmuş Symfony konteynerinde bulunan PaymentService örneği, sıradan değiştirilebilir bir PHP nesnesidir. Üzerine özgürce yazabilirsiniz.
Slim, Laminas, Yii, CodeIgniter, CakePHP — bu frameworklerin hiçbirinde servisler için çalışma zamanı değişmezlik mekanizması yoktur. Bu kavram onlarda mevcut değildir.
Ecosistem, bu soruna her zaman kod incelemesi ve konvansiyonlar ile yanıt vermiştir. Singletonları mutasyona uğratma. Bunu ekip wiki’sine ekle. Stil kılavuzuna ekle.
Frameworklerin mimariyi zorlaması gerektiğini düşünüyoruz, bunu belgelerle değil.
PHP 8.2’de readonly Var — Bu Yeterli Değil mi?
PHP 8.2’de
readonly Var — Bu Yeterli Değil mi?Donmuş Servisler’i tanımladığımızda aldığımız ilk soru budur. Adil bir soru ve doğrudan bir yanıtı hak ediyor.
PHP 8.2’deki readonly class özellikleri yapıcıda hemen dondurur. Yeni ve dondurulmuş arasındaki pencere yoktur — nesne var olduğu anda, değişmezdir.
Bu mükemmel görünse de, PHP uygulamalarının yapısıyla ilgili temel bir probleme yol açar.
Servisleriniz new zamanında yapılandırılmıyor. Yapılandırmaları config(), env() gibi kaynaklardan yapılıyor; bunlar framework başlatıldıktan sonra mevcut olan değerlerdir. Standart ServiceProvider modeli — Laravel, Doppar ve modern PHP frameworklerin belkemiği — servisleri oluşturduktan sonra yapılandırmayı gerektirir.
// Her framework bu şekilde çalışır — ve readonly bunu bozar
readonly class PaymentService
{
public string $gateway;
public float $taxRate;
}
// ServiceProvider içindeki:
$payment = new PaymentService();
$payment->gateway = config('payment.gateway'); // Fatal error: readonly property
$payment->taxRate = config(); // Fatal error: readonly property
ServiceProvider yapılandırma modeli ile readonly kullanamazsınız. Özellikleri kurucu aracılığıyla geçmeniz gerektiği için (ki bu, new zamanında bilmeniz gereken değerler demektir, bu genellikle imkânsızdır) ya tüm değerleri geçmeniz ya da readonly‘dan tamamen vazgeçmeniz zorundasınız.
Doppar’ın Donmuş Servisleri tam olarak bu boşluğu doldurur. Servis başlatma sırasında değiştirilebilir — sağlayıcılar onu özgürce yapılandırır — ve ardından, herhangi bir istek kodu çalışmadan önce dondurulur.
// Doppar'ın yaklaşımı — yapılandır sonra kilitle
use Phaseolies\DI\Attributes\Immutable;
use Phaseolies\DI\Concerns\EnforcesImmutability;
#[Immutable]
class PaymentService
{
use EnforcesImmutability;
public string $gateway = 'stripe';
public float $taxRate = 0.08;
public bool $liveMode = false;
}
// ServiceProvider içindeki:
$payment = new PaymentService();
$payment->gateway = config(); // ✓ başlatma sırasında yazılabilir
$payment->taxRate = config(); // ✓ başlatma sırasında yazılabilir
$payment->liveMode = env() === ; // ✓ başlatma sırasında yazılabilir
$this->app->singleton(PaymentService::class, fn() => $payment);
// freeze() çağrıldı → tüm istek yaşam döngüsü için kalıcı olarak kilitlendi
Bu, başlatma penceresidir. Özgürce yapılandırın. Ardından framework bunu kilitler. readonly bunu yapamaz. Diğer hiçbir framework bunu yapmaz.
Geliştirici Deneyimi
Geliştirici Deneyimi
Bunu mümkün olduğunca az boilerplate ile yapmak için çok çalıştık.
Serviste Yazdıklarınız
Serviste Yazdıklarınız
use Phaseolies\DI\Attributes\Immutable;
use Phaseolies\DI\Concerns\EnforcesImmutability;
#[Immutable]
class PaymentService
{
use EnforcesImmutability;
public string $gateway = 'stripe';
public float $taxRate = 0.08;
public bool $liveMode = false;
public function charge(float $amount): array
{
return [
'gateway' => $this->gateway,
'total' => round($amount * (1 + $this->taxRate), 2),
'status' => 'charged',
];
}
}
Normal bir servis sınıfına eklenen iki şey: #[Immutable] attribute ve use EnforcesImmutability. Özellikler public kalır. Görünürlük değişiklikleri yok. Kurucu boilerplate yok. Uygulama yapılacak bir arayüz yok.
Kontrolörde Yazdıklarınız
Kontrolörde Yazdıklarınız
class PaymentController extends Controller
{
public function __construct(
private readonly PaymentService $payment
) {}
public function process(Request $request): array
{
return $this->payment->charge($request->amount); // Önceden olduğu gibi çalışıyor
}
}
Herhangi bir kontrolör veya başka bir tüketici için değişiklik yok. Tam olarak önceki gibi enjekte edin. Özelliklere tam olarak önceki gibi erişin. Donmuş servis, normal bir servisten ayrılmaz ama yazmaya çalıştığınızda hata fırlatır.
Şimdi Mutasyon Nasıl Görünüyor
Şimdi Mutasyon Nasıl Görünüyor
public function update(int $id, Request $request, PaymentService $payment): array
{
try {
$payment->taxRate = 0.0; // Mutasyon denemesi
} catch (ImmutableViolationException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'currentRate' => $payment->taxRate, // Hala 0.08 — değişmemiş
];
}
return ['success' => true];
}
Yanıt:
{
"success": false,
"error": "Cannot mutate property $taxRate on immutable service [App\\\\Services\\PaymentService]. Services marked #[Immutable] are frozen after instantiation.",
"currentRate": 0.08
}
İstisna türlenmiş. Mesaj, sınıfı ve özelliği adlandırıyor. Değer değişmemiş. Önceden sessizce başarılı olan hata, şimdi yüksek sesle, hemen, bir yığın iz ile başarısız olur.
Tam Yaşam Döngüsü
Tam Yaşam Döngüsü
┌─────────────────────────────────────────────────────────┐
│ BAŞLATMA EVRESİ │
│ │
│ $payment = new PaymentService(); │
│ $payment->gateway = config('payment.gateway'); ✓ │
│ $payment->taxRate = config('payment.tax_rate'); ✓ │
│ $payment->liveMode = env('APP_ENV') === 'prod'; ✓ │
│ │
│ $this->app->singleton(PaymentService::class, ...); │
│ │ │
│ ▼ │
│ Container calls freeze() │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ÇALIŞMA EVRESİ │
│ │
│ $payment->gateway // ✓ 'stripe' │
│ $payment->taxRate // ✓ 0.08 │
│ $payment->charge(100.00) // ✓ method çağrıları çalışıyor │
│ │
│ $payment->gateway = 'pp' // ✗ ImmutableViolation │
│ $payment->taxRate = 0.0 // ✗ ImmutableViolation │
│ unset($payment->gateway) // ✗ ImmutableViolation │
│ clone $payment // ✗ ImmutableViolation │
└─────────────────────────────────────────────────────────┘
Yan Yana Karşılaştırma
Yan Yana Karşılaştırma
| Değiştirilebilir Singleton | readonly class | Doppar #[Immutable] | |
|---|---|---|---|
config() / env() üzerinden yapılandırılabilir | ✓ | ✗ | ✓ |
| Çalışma zamanı değişikliğinden korunur | ✗ | ✓ | ✓ |
| ServiceProvider modeli ile çalışır | ✓ | ✗ | ✓ |
| İhlal için açıkça türlendirilmiş istisna | ✗ | ✗ (genel Error) | ✓ |
| Başlatma penceresi | — | ✗ | ✓ |
| Okuma işleri şeffaf bir şekilde çalışır | ✓ | ✓ | ✓ |
| Metod çağrıları şeffaf bir şekilde çalışır | ✓ | ✓ | ✓ |
| Framework tarafından zorlanır | ✗ | Motor tarafından zorlanır | Container tarafından zorlanır |
| Enjeksiyon ile çalışır (tip ipuçları) | ✓ | ✓ | ✓ |
Daha Büyük Resim
Daha Büyük Resim
Donmuş Servisler, sadece bir kolaylık özelliği değildir. PHP’de uygulama mimarisine yönelik farklı bir düşünme biçimini temsil eder.
Çoğu framework, servisleri nesne olarak ele alır — bunlar ne zaman olursa olsun herhangi bir kod tarafından oluşturulabilir, yapılandırılabilir, enjekte edilebilir ve değiştirilebilir. Geliştirici, paylaşılan servislerin yanlışlıkla değiştirilmeyeceğini sağlamakla sorumludur. Framework’ün bu konuda bir görüşü yoktur.
Doppar artık bu konuda bir görüşü var.
Yapılandırılan ve paylaşılan servisler, yapılandırma sonrasında değişmez olmalıdır. Başlatma aşaması kurulum içindir. Çalışma aşaması kullanıma yöneliktir. Bu sınır, framework tarafından herhangi bir stil kılavuzundaki yorumdan, kod incelemesinden veya kimsenin hata yapmaması umudundan ziyade zorunlu hale getirilmelidir.
Bu, readonly özellikleri ve değişmez değer nesneleri gibi, alan tabanlı tasarımda değerli olan ilkeye benzer — en yüksek mutasyon riski olan ve sessiz bir mutasyonun sonuçlarının en ağır olduğu hizmet katmanında uygulanmıştır.
PHP uygulama servislerinin bu şekilde çalışması gerektiğine inanıyoruz. Diğer frameworklerin de benzer bir şey uygulayacağını düşünüyoruz. Ve bunu bugün Doppar ile sunuyoruz.
Daha Fazla Bilgi Edinin
Daha Fazla Bilgi Edinin
Tam dokümantasyon için Donmuş Servisler
Kaynak: Orijinal Makale


