While working on calendar applications, and using our System Calendar module, we got a few questions from our customers, how to deal with events that occur regularly – every day, week or month. There are a few ways to approach this, and this article will show one of them.


Example from Google Calendar

Generally, we want to achieve something that Google Calendar does – you add an event, specify that it’s recurrent, and then it appears on your calendar every time it’s needed.

google calendar recurring events

google calendar recurring events view

That’s the easy part. Now, if we want to edit some event in the future, calendar should ask us whether we want to change all the other events in the future for this pattern.

google calendar recurring event edit


Example with Laravel + FullCalendar

So, we’ve created a similar project in Laravel (Github repository link will be at the end of the article), which works this way.

Part 1. When creating an event, system asks whether it’s recurring.

Laravel calendar recurring events

If the event is recurring, then the system automatically generates X events in the future, one year ahead. If it’s a daily event, then in the DB you would have 365 events, for weekly event there will be 52 records, and monthly event will create 12 entries in DB.

All recurring events are created with field events.event_id value of “parent” event.

Then our FullCalendar-based calendar shows events like this:

Laravel FullCalendar recurring events

Part 2. When editing one of the recurring events, the system will ask for multi-update.

Laravel recurring events update

Part 3. Same for deleting recurring events, the system will raise a question.

It’s only one way of dealing with recurring events, you may choose to create ONE event only, with some additional settings, but I think this auto-recurring thing is more convenient.

Also, you may write a script that creates more recurring events ahead when that one year view we chose is approaching to the end.


How it works in the code

A few things to show you here:

DB table migration:

Schema::create('events', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->datetime('start_time');
    $table->datetime('end_time');
    $table->string('recurrence'); // daily/weekly/monthly/none
    $table->unsignedInteger('event_id')->nullable(); // foreign key to itself
    $table->foreign('event_id')->references('id')->on('events');
    $table->timestamps();
    $table->softDeletes();
});

Then, in app/Event.php model we have a relationship to itself:

class Event extends Model
{

    public function events()
    {
        return $this->hasMany(Event::class, 'event_id', 'id');
    }

    public function event()
    {
        return $this->belongsTo(Event::class, 'event_id');
    }
}

Controller methods in EventsController.php for storing and editing events are very simple:

public function store(StoreEventRequest $request)
{
    Event::create($request->all());
    return redirect()->route('admin.systemCalendar');
}

public function update(UpdateEventRequest $request, Event $event)
{
    $event->update($request->all());
    return redirect()->route('admin.systemCalendar');
}

And then the whole “magic” is in the Observer file app/Observers/RecurrenceObserver.php, here’s the full code:

class RecurrenceObserver
{

    public static function created(Event $event)
    {
        if(!$event->event()->exists())
        {
            $recurrences = [
                'daily'     => [
                    'times'     => 365,
                    'function'  => 'addDay'
                ],
                'weekly'    => [
                    'times'     => 52,
                    'function'  => 'addWeek'
                ],
                'monthly'    => [
                    'times'     => 12,
                    'function'  => 'addMonth'
                ]
            ];
            $startTime = Carbon::parse($event->start_time);
            $endTime = Carbon::parse($event->end_time);
            $recurrence = $recurrences[$event->recurrence] ?? null;

            if($recurrence)
                for($i = 0; $i < $recurrence['times']; $i++)
                {
                    $startTime->{$recurrence['function']}();
                    $endTime->{$recurrence['function']}();
                    $event->events()->create([
                        'name'          => $event->name,
                        'start_time'    => $startTime,
                        'end_time'      => $endTime,
                        'recurrence'    => $event->recurrence,
                    ]);
                }
        }
    }

    public function updated(Event $event)
    {
        if ($event->events()->exists() || $event->event)
        {
            $startTime = Carbon::parse($event->getOriginal('start_time'))->diffInSeconds($event->start_time, false);
            $endTime = Carbon::parse($event->getOriginal('end_time'))->diffInSeconds($event->end_time, false);
            if($event->event)
                $childEvents = $event->event->events()->whereDate('start_time', '>', $event->getOriginal('start_time'))->get();
            else
                $childEvents = $event->events;

            foreach($childEvents as $childEvent)
            {
                if($startTime)
                    $childEvent->start_time = Carbon::parse($childEvent->start_time)->addSeconds($startTime);
                if($endTime)
                    $childEvent->end_time = Carbon::parse($childEvent->end_time)->addSeconds($endTime);
                if($event->isDirty('name') && $childEvent->name == $event->getOriginal('name'))
                    $childEvent->name = $event->name;
                $childEvent->saveQuietly();
            }
        }

        if($event->isDirty('recurrence') && $event->recurrence != 'none')
            self::created($event);
    }

    public function deleted(Event $event)
    {
        if($event->events()->exists())
            $events = $event->events()->pluck('id');
        else if($event->event)
            $events = $event->event->events()->whereDate('start_time', '>', $event->start_time)->pluck('id');
        else
            $events = [];
        
        Event::whereIn('id', $events)->delete();
    }
}

We register the observer in app/Providers/AppServiceProvider.php:

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Event::observe(RecurrenceObserver::class);
    }
}

Final thing is a function $childEvent->saveQuietly(); fired from the updated() method of the observer. It has a purpose of updating the event and not fire observer again. So, inside of app/Event.php we have this:

public function saveQuietly()
{
    return static::withoutEvents(function () {
        return $this->save();
    });
}

So, that’s it for the demo. You can find the repository on Github and play around, or tailor to your needs.

You can read more about Eloquent Observers in the official Laravel documentation here.

Finally, you can read about general logic of storing recurring events here on StackOverflow, as it’s a problem not only for Laravel or FullCalendar in particular.