Günlük işlerimden çoğu, kickoff – benim Laravel başlangıç kitim üzerinde kullanıcı yönetimi yenilenmesine harcandı. Açılır CRUD panelleri, toplu işlemler, izin ataması ve ele almak istediğim bölüm: hesap durumu. Aktif, askıya alınmış, doğrulanmamış, silinmiş.
İlginç kısım, özelliğin kendisi değil. Asıl önemli olan, altında yatan modelleme kararları. Bu dört durum aslında nerede yaşıyor?
Kısır döngü: bir status sütunu
Kısır döngü: bir
status sütunuAçık bir adım, users tablosuna bir status enum sütunu eklemektir. Birini askıya alırken suspended olarak ayarlayın, işini yeniden kurduğunuzda active ile geri getirin, e-posta doğrulanmasını beklerken unverified ayarlayın ve silindiğinde ise deleted olarak belirleyin.
Bu işliyor. Ta ki çalışmayana kadar. Artık bir status sütununuz ve bir email_verified_at sütununuz ve soft delete’ler için bir deleted_at sütununuz var – ve bu üçü çelişen gerçekleri kodluyor. Bir kullanıcıyı soft-delete yapıp status değerini değiştirmeyi unutursanız, artık veritabanınız hem active hem de silinmiş diye söylüyor. E-postayı doğrulayın ama durum güncellemesi isteği ortasında başarısız olursa? Drift. Kullanıcıyı değiştiren her yer, status‘u senkronize tutmayı hatırlamak zorunda. Bu bir özellik değil, bu bakım vergisidir.
İşaretler zaten mevcut. email_verified_at doğrulandı veya doğrulanmadı bilgisini verir. deleted_at (soft delete’lerden) kaldırıldı veya kaldırılmadığı bilgisini gösterir. Gerçekten yeni olan durum, suspended – yönetici tarafından kasten girişin engellendiğidir. Bu yüzden saklanmaya değer olan tek şey budur.
Ne saklarız: bir nullable timestamp
Ne saklarız: bir nullable timestamp
Schema::table('users', function (Blueprint $table) {
$table->timestamp('suspended_at')->nullable()->after('email_verified_at');
});
Bu, tüm migrasyondur. Bir boolean is_suspended değil – bir nullable timestamp. Null, askıya alınmadığı anlamına gelir; bir değer, askıya alındığı anlamına gelir ve ne zaman olduğunu söyler. Boolean, bu ikinci gerçeği kaybeder; timestamp, ekstra maliyet olmadan bunu tutar. email_verified_at ve deleted_at ile aynı içgüdü – Laravel modelleri “bu oldu mu ve ne zaman” bilgilerini her yerde nullable timestamp olarak saklar, bu yüzden framework’ün genel akışına uyuyoruz.
Model üzerindeki davranış küçülür:
public function isSuspended(): bool
{
return $this->suspended_at !== null;
}
public function suspend(): void
{
$this->forceFill(['suspended_at' => now()])->save();
}
public function unsuspend(): void
{
$this->forceFill(['suspended_at' => null])->save();
}
Durum türetilmiştir, asla saklanmaz
Durum türetilmiştir, asla saklanmaz
Burada yol; status(), bir sütun okuma değil – sinyaller üzerinde, öncelik sırasına göre bir match biçimindedir:
public function status(): UserStatus
{
return match (true) {
$this->trashed() => UserStatus::DELETED,
$this->isSuspended() => UserStatus::SUSPENDED,
$this->email_verified_at === null => UserStatus::UNVERIFIED,
default => UserStatus::ACTIVE,
};
}
match (true), bir cond gibi okunur – koşulunun true olduğu ilk kısım kazanır, böylece öncelik sırası belirlenir. Silinmiş, askıya alınmış, doğrulanmamış, aktif sıralaması ile ilerler. Bir silinmiş ve askıya alınmış kullanıcı DELETED olarak değerlendirilir ki bu istenendir: en güçlü gerçek kazanır ve karar veren tek bir yer vardır. Drift yoktur, çünkü senkronize tutacak hiçbir şey yoktur – durum, diğer Laravel parçalarının zaten sizin için sürdüğü sütunlardan tazelenerek hesaplanır.
Sorgulama aynı muameleyi alır, böylece “aktif” bir liste sorgusunda olduğu gibi tekil modelde de aynı anlama gelir:
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('suspended_at')->whereNotNull('email_verified_at');
}
public function scopeSuspended(Builder $query): Builder
{
return $query->whereNotNull();
}
Enum kendi sunumunu taşır
Enum kendi sunumunu taşır
UserStatus, bir string destekli enum’dur, ancak bir Contract uygulamakta ve label() / color() / description() yüzeyini sunan bir InteractsWithEnum trait’ini çekmektedir (benim traitify paketimden) :
enum UserStatus: string implements Contract
{
use InteractsWithEnum;
case ACTIVE = ;
case SUSPENDED = ;
case UNVERIFIED = ;
case DELETED = ;
public function color(): string
{
return match ($this) self::ACTIVE => ,
self::SUSPENDED => ,
self::UNVERIFIED => ,
self::DELETED => ,
};
}
// label() ve description() aynı match şekli takip eder
}
Sonuç olarak, görünüm durum üzerinde dallanmaz. Enum’dan sorar:
{{ $user->status()->label() }}
Gelecek bir BANNED durumu eklemek istediğinizde, yalnızca bir dosyayı – enum’u – düzenlersiniz; her Blade şablonunu güncellemeye gerek kalmaz. Sunum, tanımladığı verilerle birlikte yaşar, ki bu da enum’a yöntemler vererek match bloklarını UI etrafında yaymanın asıl amacı olacaktır.
Durumu türetmek, bunu zorlamak değildir
Durumu türetmek, bunu zorlamak değildir
Hesaplanan bir status() bir etiket gibidir. Bu askıya alınmış bir kullanıcının mevcut oturumunu kullanmasını engellemez – askıya alındıklarında, oturumda zaten oturum açmışlardı ve çerez bu enum’dan etkilenmez. Uygulama, middleware aracılığı ile ayrı ve kasten zorlanan bir sınırdır.
class EnsureUserIsNotSuspended
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user && $user->isSuspended()) {
Auth::guard()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()
->route()
->with(, ());
}
return $next($request);
}
}
Not edin, yalnızca yönlendirme yapmaz – aynı zamanda logout(), oturumu geçersiz kılma ve CSRF tokenini yeniden oluşturma işlemlerini gerçekleştirir. Askıya almak, sürgün etmeli, basit bir rahatsızlık oluşturmak yerine. Yalnızca bir yönlendirme, çerezlerde geçerli bir oturum bırakabilir.
Pest: önceliği ve çıkışı belirleme
Pest: önceliği ve çıkışı belirleme
Kapalı tutmayı gerektiren iki şey vardır – bu match önceliğinin geçerli kalması ve middleware’in gerçekten askıya alınmış bir oturumu dışarı atması.
it(, function () {
$user = User::factory()->create([=> now()]);
expect($user->status())->toBe(UserStatus::ACTIVE);
$user->suspend();
expect($user->fresh()->status())->toBe(UserStatus::SUSPENDED);
$user->delete(); expect($user->fresh()->status())->toBe(UserStatus::DELETED);
});
it(, function () {
$user = User::factory()->create([=> now()]);
$user->suspend();
$this->actingAs($user)
->get()
->assertRedirect(route());
expect(auth()->check())->toBeFalse();
});
İlk test, zamanla değer kazanır: öncelik sırasını dondurur, böylece gelecekteki bir yeniden yapılandırma, SUSPENDED durumunu DELETED‘den öncelikli hale getiremez.
Çıkarım
Çıkarım
Saklamanız gereken bir şeyi türetmeyin. Bir status sütunu uygun görünür, fakat dört sütuna dönüşür ve bunlar birbirleriyle çelişir. Diğerlerini email_verified_at ve deleted_at üzerinden alan yalnızca suspended_at için bir nullable timestamp saklayın ve status()‘u öncelik sırası ile sonuçlanacak match (true) ile hesaplayarak ve tek bir gerçeklik kaynağı olmasını sağlayın.
Sonunda atlanması kolay olan kısmı hatırlayın: bir durumu türetmek ve zorlamak iki ayrı işdir. Enum, hesabı etiketler; middleware ise gerçekten askıya alınmış bir kullanıcıyı dışarı atan yapıdır.
Kaynak: Orijinal Makale


