İçeriğe geç
Muhammet Şafak
Günlük 3 dk okuma

Laravel ile takvim/etkinlik özelliği kurmak

Tarih, tekrar kuralı ve zaman dilimi içeren bir takvim özelliğini Laravel'de uçtan uca kurma deneyimimi paylaşıyorum.


Geçen ay üzerinde çalıştığım projede takvim özelliği eklemem gerekti. Kullanıcılar etkinlik oluşturacak, bu etkinlikler tekrarlayabilecek (her hafta Pazartesi, her ayın ilk günü gibi), ve farklı zaman dilimlerinden giriş yapan kullanıcılar görecek. Kulağa sıradan geliyor ama tarih, tekrar ve zaman diliminin üçü bir araya gelince ortaya düşündüğümden fazla karar noktası çıktı.

Veri yapısını tasarlamak

İlk yaptığım şey etkinlikleri nasıl saklayacağımı düşünmek oldu. Tekrarlayan etkinlikler için iki yaklaşım var: her tekrarı ayrı kayıt olarak saklamak ya da tek bir kural kaydı saklayıp tekrarları hesaplamak.

İlk yaklaşım sorgu yazmayı kolaylaştırıyor; ama uzak gelecekteki tekrarlar için binlerce önceden kayıt oluşturmak israfı büyük. İkinci yaklaşım daha az depolama gerektiriyor ama hesaplama katmanı karmaşıklaşıyor.

Aldığım karar: ana etkinlik bir kayıt, recurrence rule JSON sütununda saklansın, görünüm oluşturulurken hesaplansın. Bu aşırı ölçekli bir uygulama değildi; bu yaklaşım yeterliydi.

Migration bu görünümü aldı:

<?php

Schema::create('events', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->string('title');
    $table->text('description')->nullable();
    $table->datetime('starts_at');
    $table->datetime('ends_at');
    $table->string('timezone', 64)->default('UTC');
    $table->json('recurrence')->nullable();
    $table->timestamps();
});

recurrence sütunu null ise etkinlik tekrar etmiyor. Değer varsa {"frequency": "weekly", "until": "2018-12-31"} gibi bir yapı içeriyor.

Zaman dilimi meselesi

Timezone yönetimi bu projenin en can sıkıcı kısmıydı. Veritabanında her zaman UTC saklıyorum; bu net bir kural. Ama kullanıcıya gösterirken kullanıcının kendi zaman dilimine çevirmem gerekiyor.

Laravel’in Carbon kütüphanesi bu işi büyük ölçüde kolaylaştırıyor:

<?php

$event->starts_at // UTC olarak geliyor, Carbon nesnesi

// Kullanıcının zaman dilimine çevirmek
$localTime = $event->starts_at->setTimezone($user->timezone);

// Gösterim için
echo $localTime->format('d M Y, H:i');

Sorun şu: formları kullanıcı, kendi yerel saatiyle dolduruyor. O değeri veritabanına kaydetmeden önce UTC’ye çevirmem gerekiyor:

<?php

$localStartsAt = Carbon::createFromFormat(
    'Y-m-d H:i',
    $request->starts_at,
    $request->user()->timezone
);

$event->starts_at = $localStartsAt->utc();

Bu dönüşümü request doğrulaması sırasında ya da model oluşturulurken yapabilirsiniz. Ben başlangıçta controller’da yaptım; sonra bu mantığı bir form request sınıfına taşımak daha temiz göründü.

Tekrar eden etkinlikleri hesaplamak

Takvim görünümü için belirli bir tarih aralığındaki etkinlikleri çekmem gerekiyor. Tekrar etmeyen etkinlikler için doğrudan sorgu yazılabiliyor. Tekrar edenler için bir hesaplama katmanı yazmak gerekti.

Basit bir örnek: haftalık tekrar eden bir etkinliğin belirli bir ay içindeki tarihlerini bulmak.

<?php

function expandRecurringEvent(Event $event, Carbon $from, Carbon $to): array
{
    $occurrences = [];
    $recurrence   = $event->recurrence;

    if (!$recurrence || $recurrence['frequency'] !== 'weekly') {
        return [$event->starts_at];
    }

    $current = $event->starts_at->copy();
    $until   = isset($recurrence['until'])
        ? Carbon::parse($recurrence['until'])
        : $to;

    while ($current->lte($until) && $current->lte($to)) {
        if ($current->gte($from)) {
            $occurrences[] = $current->copy();
        }
        $current->addWeek();
    }

    return $occurrences;
}

Bu temel işlev; gerçek dünyada aylık, yıllık, belirli günlerde tekrar gibi kurallar eklendikçe bu mantık büyüyor. Bir noktada nesbot/carbon ile birlikte iCalendar kurallarını destekleyen harici bir paket değerlendirilebilir ama bu projede kapsam sınırlıydı, sade tutmak mümkün oldu.

Takvim görünümü

Frontend tarafında sıfırdan bir takvim bileşeni yazmak yerine FullCalendar kütüphanesini kullandım. Bu kütüphane etkinlikleri JSON formatında bekliyor; Laravel tarafında bir API ucu açıp istenen aralıktaki etkinlikleri döndürmek yeterli:

<?php

public function index(Request $request)
{
    $from = Carbon::parse($request->start);
    $to   = Carbon::parse($request->end);

    $events = Event::whereBetween('starts_at', [$from, $to])
        ->get()
        ->map(function ($event) {
            return [
                'id'    => $event->id,
                'title' => $event->title,
                'start' => $event->starts_at->toIso8601String(),
                'end'   => $event->ends_at->toIso8601String(),
            ];
        });

    return response()->json($events);
}

Deneyimden çıkardıklarım

Bu özelliği yaparken en çok zaman kaybettiğim yer zaman dilimi dönüşümüydü. Başlangıçta yerel saati direkt veritabanına yazdım, sonra takvim yanlış saatler gösterdi, geri döndüm. “Veritabanında her zaman UTC” kuralını baştan oturtmak çok önemli.

Tekrar eden etkinliklerin tasarımı da ilk düşündüğümden daha fazla karar gerektirdi. Tekrar ne zaman biter? Etkinliğin başlangıç saati tekrar hesaplanırken gün ışığından yararlanma saatini (DST) dikkate alıyor mu? Bu soruları baştan sorarsanız sonradan hata düzeltmek için harcanan zamanı büyük ölçüde azaltabilirsiniz.

Takvim özelliği kulağa basit geliyor ama içinde dikkat gerektiren birkaç köşe var. Düzgün çalışınca da tatmin edici; her şeyin doğru sıraya oturması güzel hissettiriyor.

Etiketler: #Laravel
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi