Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Calendr Laravel Package

yohang/calendr

Calendr is an object-oriented PHP calendar library for creating, managing, and rendering calendars and events. It supports recurring events, multiple calendars, and flexible output via renderers, making it easy to integrate scheduling into PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Calendr is a framework-agnostic domain library: it gives you period objects (Day, Week, Month, Year, Range), an event abstraction, and a provider mechanism to pull events into those periods. It does not ship Eloquent models, controllers, or HTML rendering — you own persistence and presentation.

composer require yohang/calendr

There is no service provider to publish. Register it once as a singleton so providers are configured in one place:

// app/Providers/AppServiceProvider.php
$this->app->singleton(\Calendr\Calendar::class, function ($app) {
    $calendar = new \Calendr\Calendar();
    $calendar->getEventManager()->addProvider($app->make(\App\Calendar\EloquentEventProvider::class));

    return $calendar;
});

First use case — a month grid with events:

use Calendr\Calendar;

$calendar = app(Calendar::class);
$month = $calendar->getMonth(2025, 12);          // Period\Month

foreach ($month as $week) {                       // Month iterates weeks
    foreach ($week as $day) {                     // Week iterates days
        $inMonth = $month->contains($day);        // false for leading/trailing days
        $events  = $calendar->getEvents($day);    // aggregated from all providers
    }
}

Look first at Calendr\Calendar (period factory + event aggregation), Calendr\Period\PeriodInterface (getBegin, getEnd, contains, getNext, getPrevious, isCurrent, format), and Calendr\Event\Provider\ProviderInterface — that interface is the only integration point most applications need.

Implementation Patterns

Provider-backed events are the core integration. Implement one provider per data source; Calendr handles the rest.

use Calendr\Event\Event;
use Calendr\Event\Provider\ProviderInterface;
use Calendr\Period\PeriodInterface;

final class EloquentEventProvider implements ProviderInterface
{
    public function getEvents(PeriodInterface $period, array $options = []): iterable
    {
        $rows = Booking::query()
            ->where('starts_at', '<', $period->getEnd())
            ->where('ends_at', '>', $period->getBegin())   // overlap, not BETWEEN
            ->when($options['user_id'] ?? null, fn ($q, $id) => $q->where('user_id', $id))
            ->get();

        foreach ($rows as $booking) {
            yield new Event(
                'booking-'.$booking->id,
                $booking->starts_at->toDateTime(),   // convert UTC -> display TZ here
                $booking->ends_at->toDateTime(),
            );
        }
    }
}

Use the options array as your filter channel. $calendar->getEvents($month, ['user_id' => $id]) forwards the second argument to every provider, so tenant scoping, visibility flags, and "include cancelled" toggles travel with the period instead of leaking into provider constructors.

Fetch once per period, then index. Calling getEvents($day) inside the week/day loop causes one provider round-trip per day. Prefer:

$byDate = [];
foreach ($calendar->getEvents($month, ['user_id' => $userId]) as $event) {
    $cursor = clone $event->getStart();
    while ($cursor <= $event->getEnd()) {          // multi-day events
        $byDate[$cursor->format('Y-m-d')][] = $event;
        $cursor->modify('+1 day');
    }
}
// in the view: $byDate[$day->format('Y-m-d')] ?? []

Navigation without date math. $month->getPrevious() / getNext() return same-type periods, and $month->isCurrent() drives the "today" highlight. For agenda views use getRange(new DateTime($from), new DateTime($to)) and render a flat list rather than a grid.

Custom events instead of mapping. If your Booking model can implement EventInterface (getUid, getStart, getEnd, containsDate, containsPeriod), return it directly and skip DTO creation; keep presentation concerns in Blade, not in the event object.

Rendering lives in your app. A Blade partial that loops weeks → days → $byDate[$day->format('Y-m-d')] is the idiomatic approach; there is no built-in HTML renderer to configure. For week grids, use $day->format('j'), $day->isCurrent(), and $month->contains($day) to dim adjacent-month cells.

Testing. Periods are pure functions of the DateTime you pass in (apart from isCurrent()), so unit-test providers with fixed dates and new Range(...) rather than mocking the clock.

Gotchas and Tips

  • Month/Year iteration includes out-of-range days. Month yields whole weeks, so the first and last week contain days from neighbouring months. Always gate with $month->contains($day) before counting events or attaching links.
  • Week numbers are ISO-8601. getWeek($year, $number) expects the ISO year, not the calendar year — week 1 of 2026 can start in December 2025. Derive keys with $date->format('o-W'), never Y-W, or your late-December views will show the wrong week.
  • Overlap SQL must match contains semantics. Events should be selected with start < periodEnd AND end > periodBegin; BETWEEN silently drops multi-day bookings that straddle the boundary.
  • DateTime is mutable. Calendr's API hands you DateTime instances; calling modify() on a value you also passed into a period can shift the period's bounds. clone before mutating, or normalize to DateTimeImmutable in your provider and convert at the edge.
  • Timezone drift at day boundaries. A booking stored in UTC at 2025-12-01 23:30 belongs to Dec 2 in Europe/Paris. Convert to the display timezone inside the provider, before constructing events, and let periods be built in that same timezone.
  • Providers are per-period, not per-day — but only if you ask once. Cache the indexed result per request (Cache::remember("cal:{$userId}:{$month->format('Y-m')}", ...)) if the same month is rendered in multiple widgets on one page.
  • Options are unvalidated and untyped. Document the keys your providers accept; a typo in user_id silently returns everything.
  • No iCal, reminders, recurrence rules, or persistence. RRULE expansion and .ics export need a separate package (eluceo/ical, spatie/icalendar-generator) — Calendr is the view-model/aggregation layer only.
  • PHP 8.2+ is a hard floor. Apps still on 8.1 cannot install it; check your CI matrix before adopting.
  • Extension points: custom ProviderInterface implementations for new data sources, EventInterface on domain models, and the calendar's period factory if you need non-Gregorian or fiscal periods.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle