PHP'de Sabit, Enum ve Readonly: 8 Aracın Tek Karar Ağacı
define'dan asymmetric visibility'ye — modern PHP'de değişmezliği doğru kurmak için üç eksende bir mental model ve somut karar matrisi.
Bir PHP geliştiricisine “bir sabit tanımla” deyince hâlâ define() yazıyorsa, son altı yılda PHP’de neler olduğunu kaçırmış demektir. Aynı geliştirici “değişmez bir nesne yarat” denildiğinde constructor’da elle atama yapıp setter’ları siliyorsa, son üç yılı da kaçırmıştır.
PHP’de bugün “bir şey tanımla, sonra değişmesin” demenin on farklı yolu var:
define()const(top-level)- Class
const(visibility ile) - Interface constants
finalclass constants- Typed class constants (PHP 8.3)
enum— pure + backed (PHP 8.1)readonlyproperty (PHP 8.1)readonly class(PHP 8.2)- Asymmetric visibility (PHP 8.4)
Çoğu mid-senior PHP geliştiricisi bunların yarısını biliyor, üçte birini doğru kullanıyor. Suçlu geliştirici değil — bu araçların tek bir mental model altında nasıl konumlandığını anlatan bir Türkçe kaynak yok.
Bu yazı o boşluğu doldurmak için. Sonunda ne hangisini ne zaman seçeceğine dair tek bir karar ağacımız olacak.
Üç Eksen: Tüm Seçimleri Belirleyen Çerçeve
“Hangisini ne zaman?” sorusunun cevabı her zaman üç eksende veriliyor. Bu üç ekseni içselleştirdiğinde, on aracın hangisinin nereye düştüğünü kendin bulabilirsin.
1. Ne zaman belirlenir? — Compile-time vs Runtime
Değer, kod yorumlanırken mi bellekte oluşacak, yoksa program çalışırken mi hesaplanacak? const compile-time’da bellekte; define() runtime’da. Bu fark hem performansı hem de kullanılabileceği yerleri belirler — örneğin const koşullu bir blok içinde tanımlanamaz; define() tanımlanabilir.
2. Kapsamı nedir? — Global / Namespace / Class / Instance
Değer nerede yaşayacak? Tüm uygulamaya mı yayılacak (global), bir namespace içinde mi (namespace-scoped), belirli bir sınıfa mı (class), yoksa o sınıfın bir örneğine mi (instance) ait olacak? Kapsam ne kadar dar olursa, kodun o kadar disiplinli olur.
3. Davranış taşıyabilir mi? — Sade Değer vs Metodlu Tip
Bu sabit sadece bir veri parçası mı, yoksa kendi davranışı (metodları, ilişkili sabitleri) olan bir tip mi olmalı? class const sade bir değerdir; enum ise tip + metod taşıyabilen bir yapıdır. Bu eksen, “kötü kullanılmış class const’lar”ın enum ile değişeceği noktayı işaret eder.
İşte on aracın bu üç eksendeki konumu:
| Araç | PHP | Zaman | Kapsam | Davranış |
|---|---|---|---|---|
define() | her zaman | runtime | global | sade |
const (top-level) | her zaman | compile | namespace | sade |
Class const | 7.1+ | compile | class | sade |
| Interface const | her zaman | compile | paylaşılan | sade |
final class const | 8.1+ | compile | class (override yok) | sade |
| Typed class const | 8.3+ | compile | class (tip güvenli) | sade |
enum | 8.1+ | compile | class | tip + metod |
readonly prop | 8.1+ | runtime | instance | sade |
readonly class | 8.2+ | runtime | instance (tümü) | sade |
| Asymmetric visibility | 8.4+ | runtime | instance | ”yumuşak readonly” |
Şimdi her birini ayrı ayrı, kısa örneklerle geçelim.
1. define() — Runtime, Global, Sade
define('APP_NAME', 'MuhammetSafak');
define('MAX_RETRIES', 3);
Runtime’da, global kapsamda, sade bir değer. PHP 4 günlerinden kalan ilk araç. Koşullu blok içinde tanımlanabilir:
if ($env === 'production') {
define('LOG_LEVEL', 'error');
} else {
define('LOG_LEVEL', 'debug');
}
Ne zaman kullanılır: Bugün, neredeyse hiç. Tek meşru kullanım — değerin koşullu olarak veya runtime hesaplamayla belirlenmesi gerektiği nadir durumlar. Onun dışında her yerde const veya bir config nesnesi tercih edilir.
Yaygın yanlış: “Global ayarlar define’la tutulur” alışkanlığı. Bu, 2010 PHP’sinden kalma bir refleks. Bugünün doğrusu typed bir config sınıfı veya env-injection.
2. const (Top-Level) — Compile-Time, Namespace, Sade
namespace App\Config;
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
const SUPPORTED_LOCALES = ['tr', 'en', 'de'];
Compile-time’da belirlenir, namespace’e ait, sade değer. Koşullu blok içinde tanımlanamaz çünkü compile-time’da çözülmesi gerekir.
Ne zaman kullanılır: Bir namespace içinde, gerçekten sabit ve sınıfa ait olmayan bir değer için. Pratikte: dosya kapsamlı yardımcı sabitler.
Yaygın yanlış: Top-level const ile class-level const karıştırılır. İkincisi daha yaygın ve daha doğru — sınıfla mantıksal olarak bağlı sabitler her zaman class içine gitmeli.
3. Class const (Visibility ile) — Compile-Time, Class, Sade
class HttpClient
{
public const DEFAULT_TIMEOUT = 30;
protected const RETRY_BACKOFF_MS = 250;
private const INTERNAL_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0';
}
PHP 7.1’den beri visibility (public, protected, private) destekleniyor. Sınıfla mantıksal olarak ilişkili, compile-time’da çözülen sade değer.
Ne zaman kullanılır: Bir sınıfın konfigürasyonu niteliğindeki değerler. “Bu sınıfın iç işleyişini ayarlayan, dışarıdan değişmemesi gereken” şeyler.
Yaygın yanlış: Status/role/type değerlerini class const olarak tanımlamak:
class Order
{
public const STATUS_PENDING = 1;
public const STATUS_PAID = 2;
public const STATUS_SHIPPED = 3;
public const STATUS_CANCELLED = 4;
}
Bu kodu hâlâ yazıyorsan, sıradaki maddeye dikkat et — enum tam olarak bu sorunu çözmek için var.
4. Interface Constants — Compile-Time, Paylaşılan, Sade
interface CacheableInterface
{
public const DEFAULT_TTL = 3600;
public const MAX_TTL = 86400 * 30;
}
class RedisCache implements CacheableInterface
{
public function get(string $key, int $ttl = self::DEFAULT_TTL): mixed
{
// ...
}
}
Birden fazla sınıfın paylaşacağı sabitler için interface kullanılır.
Ne zaman kullanılır: Aynı sabitin birden fazla implementasyonda kullanılacağı, sözleşmenin parçası olduğu durumlar.
Yaygın yanlış: Interface’i sadece sabit dağıtmak için kullanmak. Interface davranış sözleşmesidir, sabit kutusu değil. Sadece sabit barındıran interface — kötü sinyal.
5. final Class Constants — Compile-Time, Class (Override Yok), Sade
class PaymentGateway
{
final public const API_VERSION = 'v2';
}
class StripeGateway extends PaymentGateway
{
// public const API_VERSION = 'v3'; // Fatal error
}
PHP 8.1’den beri class constants final olabiliyor. Alt sınıflar override edemez.
Ne zaman kullanılır: Sabitin alt sınıflarda değişmemesi gerektiğinden eminsen. Inheritance’da yanlış override’ları engellemek için.
Yaygın yanlış: Her sabite final yapıştırmak. final bir kararı temsil eder — “bu noktadan sonra değişmez.” Her yerde kullanmak değil, gerektiğinde kullanmak doğru.
6. Typed Class Constants — Compile-Time, Class, Sade (Tip Güvenli)
class Logger
{
public const string LEVEL_INFO = 'info';
public const int MAX_BUFFER = 1000;
public const array CHANNELS = ['file', 'syslog', 'stderr'];
}
class JsonLogger extends Logger
{
// public const int LEVEL_INFO = 5; // TypeError
}
PHP 8.3’ün getirdiği uzun zamandır beklenen özellik. Class const’lara tip belirtilebiliyor. Override edildiğinde tip uyumu zorunlu.
Ne zaman kullanılır: PHP 8.3+ kullanıyorsan, her class const’ta. Tip yazmak ekstra üç karakter; verdiği güvence çok daha fazla.
Yaygın yanlış: PHP 8.3+ projede typed olmayan eski class const’ları olduğu gibi bırakmak. Migration kolay, otomatik düzeltici araçlar (Rector) bunu tek geçişte yapabiliyor.
7. enum — Tip + Metod, Sınırlı Küme
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
self::Pending => 'Bekliyor',
self::Paid => 'Ödendi',
self::Shipped => 'Kargoya verildi',
self::Cancelled => 'İptal edildi',
};
}
public function isTerminal(): bool
{
return in_array($this, [self::Shipped, self::Cancelled]);
}
}
function processOrder(Order $order, OrderStatus $newStatus): void
{
if ($order->status->isTerminal()) {
throw new LogicException('Tamamlanmış sipariş değiştirilemez.');
}
// ...
}
PHP 8.1’in getirdiği en büyük dönüşüm. Tip + sınırlı küme + davranış tek pakette. Backed enums (string/int değerlerle) veritabanı/JSON eşlemesi için; pure enums (sadece case’ler) memory-only state için.
Ne zaman kullanılır: “Bu değer sadece şu birkaç şeyden biri olabilir” dediğin her yerde. Status, role, type, priority, color, currency, language — hepsi enum adayı.
Yaygın yanlış: Bir önceki bölümde gösterdiğim const STATUS_PENDING = 1 pattern’ini sürdürmek. Eğer hâlâ böyle yazıyorsan: php artisan veya benzer migration aracıyla bugün geçiş yap. Bu, PHP 8.x’in en büyük üretkenlik kazanımlarından biri.
8. readonly Property — Runtime, Instance, Sade
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
public function add(Money $other): Money
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException('Farklı para birimleri toplanamaz.');
}
return new Money($this->amount + $other->amount, $this->currency);
}
}
$price = new Money(100, 'TRY');
// $price->amount = 200; // Error: Cannot modify readonly property
$discounted = $price->add(new Money(-20, 'TRY')); // Yeni nesne, eski değişmedi
PHP 8.1’den beri property’ler readonly olabiliyor. Kurucuda set edilir, sonrasında değişmez. Value object pattern’inin PHP’deki doğal karşılığı.
Ne zaman kullanılır: Bir nesnenin oluşturulduktan sonra belirli özelliklerinin değişmemesi gerektiği her yerde. DTO’lar, value object’ler, configuration object’ler.
Yaygın yanlış: Setter’ları silip __set’i throw etmekle “immutability” yapmaya çalışmak. PHP 8.1+ kullanıyorsan bu pattern artık eski; readonly aynı sonucu dilin garantisiyle veriyor.
9. readonly class — Runtime, Instance (Tümü), Sade
final readonly class UserDto
{
public function __construct(
public int $id,
public string $name,
public string $email,
public DateTimeImmutable $createdAt,
) {}
}
PHP 8.2’den beri sınıfın tamamı readonly ilan edilebiliyor. Her property otomatik olarak readonly olur, ayrı ayrı yazmaya gerek kalmaz.
Ne zaman kullanılır: Tüm property’leri immutable olan sınıflar için. Tipik adaylar: DTO, value object, event payload, command/query object.
Yaygın yanlış: readonly class ile birlikte mutable koleksiyon (array, ArrayObject) tutmak. readonly sadece property referansını sabitler — referansın gösterdiği nesnenin iç durumunu sabitlemez. Mutable bir array’i readonly bir property’de tutarsan, dizinin içeriği hâlâ değiştirilebilir.
final readonly class TagSet
{
public function __construct(
public array $tags, // readonly ama içerik mutable!
) {}
}
$set = new TagSet(['a', 'b']);
// $set->tags = []; // Error
$tags = $set->tags;
$tags[] = 'c'; // $set->tags etkilenmez (PHP array kopyalanır)
// Ama nesne tutuyorsa:
$obj = new stdClass();
$obj->value = 1;
$set2 = new SomeReadonly($obj);
$set2->obj->value = 999; // Çalışır! readonly nesne referansını korur, içeriğini değil.
Bu nüans çok önemli — readonly derin değil, sığ değişmezlik sağlar.
10. Asymmetric Visibility — Runtime, Instance, “Yumuşak Readonly”
final class User
{
public function __construct(
public private(set) string $email,
public private(set) DateTimeImmutable $lastLoginAt,
) {}
public function recordLogin(DateTimeImmutable $at): void
{
$this->lastLoginAt = $at; // Sınıf içinden yazılabilir
}
}
$user = new User('test@example.com', new DateTimeImmutable());
echo $user->email; // Dışarıdan okunabilir
// $user->email = 'foo@bar'; // Error: Cannot modify
$user->recordLogin(new DateTimeImmutable()); // İçeriden değiştirilebilir
PHP 8.4’ün getirdiği yeni özellik. Property’nin okuma görünürlüğü ile yazma görünürlüğü ayrı tanımlanabilir. public private(set) = dışarıdan okunur, sadece sınıf içinden yazılır.
Ne zaman kullanılır: readonly’nin “hiç değişemez” katılığı çok sıkı geldiğinde. Bir property’nin dışarıdan değişmesini istemiyorsun ama iç metodlar yazabilsin istiyorsan.
Yaygın yanlış: Bunu private property + public getter pattern’inin yerine koymak. Aslında koymalısın, ama “her readonly’yi asymmetric’e dönüştür” değil. readonly ve asymmetric visibility farklı niyetleri ifade eder — birincisi “asla değişmez”, ikincisi “dışarıdan değişmez, içeriden değişebilir.”
Karar Ağacı: Hangisini Ne Zaman?
İşte birikenleri tek bir akışta toplayan karar süreci:
1. Değer runtime'da mı belirlenecek?
├─ Evet → define()
└─ Hayır (compile-time) → 2'ye git
2. Bir sınıfa mı ait?
├─ Hayır (namespace-scoped) → const (top-level)
└─ Evet → 3'e git
3. Sonlu bir küme mi (status, role, color vs.)?
├─ Evet → enum (backed veya pure)
└─ Hayır → 4'e git
4. Birden fazla sınıfta paylaşılacak mı?
├─ Evet → interface constant
└─ Hayır → 5'e git
5. PHP 8.3+ kullanıyor musun?
├─ Evet → typed class constant
└─ Hayır → class constant (visibility ile)
6. Override edilmemeli mi?
└─ Evet → yukarıdakine final ekle
Nesne tarafı ayrı bir akış:
A. Bir nesnenin property'lerinin değişmemesini istiyorum.
├─ Tüm property'ler immutable mı? → readonly class
└─ Sadece bazıları → readonly property
B. Property dışarıdan okunsun ama sadece içeriden değişsin.
└─ PHP 8.4+ → public private(set) (asymmetric visibility)
└─ PHP 8.3 ve altı → private property + public getter
Anti-Patterns: PHP Ekosisteminde Sık Gördüğüm Yanlışlar
1. Status sabitleri için class const
// Yanlış (2026 itibarıyla)
class Order
{
public const STATUS_PENDING = 1;
public const STATUS_PAID = 2;
public const STATUS_SHIPPED = 3;
}
// Doğru
enum OrderStatus: int
{
case Pending = 1;
case Paid = 2;
case Shipped = 3;
}
Bu tek başına bir migration projesi olabilir. Bir codebase’de yüzlerce yerde geçer ve tip güvenliğini, IDE autocomplete’ini, exhaustive match’ı topluca getirir.
2. Global config için define() yığını
// Yanlış
define('DB_HOST', 'localhost');
define('DB_NAME', 'app');
define('REDIS_HOST', '127.0.0.1');
define('MAIL_DRIVER', 'smtp');
// ... 40 satır daha
// Doğru
final readonly class DatabaseConfig
{
public function __construct(
public string $host,
public string $name,
public string $user,
) {}
}
Typed config object’ler hem test edilebilir hem de IDE tarafında autocomplete’lenir. Global state ne kadar azsa, kod o kadar deterministik olur.
3. Setter’ları silerek “immutability” yapmak
// Yanlış
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; }
// setter yok = "immutable" sandığımız şey
}
// Doğru
final readonly class Money
{
public function __construct(
public int $amount,
public string $currency,
) {}
}
İkincisi yarı uzunlukta ve dilin garantisini taşıyor. PHP 8.2+ varsa birincisini yazmaya devam etmek için bahane yok.
4. Interface’i sabit kutusu olarak kullanmak
// Yanlış
interface Constants
{
public const PI = 3.14;
public const E = 2.71;
}
class Calculator implements Constants
{
// ...
}
Interface = davranış sözleşmesi. Sabit barındırması meşru — ama sadece sabit barındırmak için interface uydurmak, yanlış soyutlama.
5. Magic number’lar her yerde
// Yanlış
if ($user->loginAttempts >= 5) {
$user->lock();
}
sleep(300);
// Doğru
final class AuthPolicy
{
public const int MAX_LOGIN_ATTEMPTS = 5;
public const int LOCKOUT_SECONDS = 300;
}
if ($user->loginAttempts >= AuthPolicy::MAX_LOGIN_ATTEMPTS) {
$user->lock();
}
sleep(AuthPolicy::LOCKOUT_SECONDS);
Bu, en eski tavsiye — ama hâlâ en çok ihlal edilen.
Migration: Eski Kodu Yeniye Taşımak
Class const status sabitlerinden enum’a
// Önce
class Order
{
public const STATUS_PENDING = 1;
public const STATUS_PAID = 2;
public int $status;
}
if ($order->status === Order::STATUS_PAID) { ... }
// Sonra
enum OrderStatus: int
{
case Pending = 1;
case Paid = 2;
}
class Order
{
public OrderStatus $status;
}
if ($order->status === OrderStatus::Paid) { ... }
Migration genelde kolay — değer eşlemesini koruduğun sürece (int backed enum) veritabanı şeması değişmez. Tek dikkat: PDO/ORM eşlemesi.
Setter’sız sınıftan readonly class’a
// Önce
class UserDto
{
private int $id;
private string $name;
public function __construct(int $id, string $name) { ... }
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
}
// Sonra
final readonly class UserDto
{
public function __construct(
public int $id,
public string $name,
) {}
}
// Çağrı: $dto->getId() yerine $dto->id
Bu migration kod tabanı genelinde getter çağrılarını değiştirmeyi gerektirir. IDE refactor’ları (PhpStorm “Inline Method”) yardımcı olur.
Global define’lardan typed config’e
// Önce
define('STRIPE_KEY', getenv('STRIPE_KEY'));
define('STRIPE_WEBHOOK_SECRET', getenv('STRIPE_WEBHOOK_SECRET'));
// Sonra
final readonly class StripeConfig
{
public function __construct(
public string $key,
public string $webhookSecret,
) {}
public static function fromEnv(): self
{
return new self(
key: getenv('STRIPE_KEY') ?: throw new RuntimeException('STRIPE_KEY missing'),
webhookSecret: getenv('STRIPE_WEBHOOK_SECRET') ?: throw new RuntimeException('STRIPE_WEBHOOK_SECRET missing'),
);
}
}
// Container'a register et, inject et
Bu migration en büyüğü ama en değerlisi. Global state’in azalması, kodun test edilebilirliğini katlar.
Performans: Önemli Ama Belirleyici Değil
Mikro-benchmark düzeyinde:
constdefine()’dan hızlıdır (compile-time çözüm)- Class const’lar değişken erişiminden hızlıdır
enuminstance’ları singleton — her case için tek nesne, karşılaştırma===ile sabit zamanlıreadonlyçok hafif overhead — sadece yazma denetimi
Pratikte: bunların hiçbiri sıradan bir web uygulamasında dar boğaz değil. Doğruyu seç, performans çoğunlukla doğrunun yan ürünü olur. “Bu daha hızlı” diye yanlış soyutlamayı seçme — okunabilirlik kaybı, mikro-saniyelik kazançtan kat kat pahalı.
İstisna: Sıcak döngülerde milyonlarca iterasyonlu kod yazıyorsan (hashlemeler, parser’lar, transformasyonlar), tek enum::label() çağrısı match overhead’i toplar — orada class const daha hızlı olabilir. Önce ölç, sonra optimize et.
Kapanış
PHP’de değişmezlik tek bir araç değil; üç eksenin (zaman, kapsam, davranış) farklı kombinasyonlarına verilmiş on farklı cevap. Hangisini seçeceğin sorusu aslında ne yapmak istiyorsun sorusudur:
- Bir değer mi tutuyorsun, bir tip mi yaratıyorsun?
- Değer ne zaman belli olacak?
- Değişmemesi gereken şey, referans mı yoksa içerik mi?
Bu üç soru sana doğru aracı verir. On aracın hepsini ezberlemen gerekmez — üç ekseni ezberle, gerisi türev.
Son olarak: bu konuyu 2022’de const ve define ekseninde ele almıştım. Aradan dört yıl geçti, PHP yedi yeni sürüm çıkardı, ekosistem dönüştü. Yazılım yazıları da eskidir — bu yazıyı 2030’da okuyorsan, muhtemelen güncellenmiş bir versiyonu da vardır.