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

Calendar Laravel Package

endroid/calendar

Endroid Calendar is a PHP library for building and rendering calendars with events and customizable date output. It provides a flexible foundation for adding calendar functionality to Laravel and PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the PHP constraint first. The package has very low adoption (8 stars, 0 known dependents), so before anything else check that its composer.json require block lines up with your Laravel version's PHP floor:

composer require endroid/calendar

If Composer resolves with --prefer-lowest warnings or conflicts, stop and read the constraint rather than forcing it. Small libraries from a single maintainer (Endroid) are usually fine, but they lag PHP minor releases.

Where to look first. There is almost certainly no Laravel service provider, facade, or config publishing — you will wire it yourself. Read, in this order:

  1. src/ — the whole tree. It is tiny; you can read it end-to-end in one sitting. You need to know the exact public surface (Calendar, Event-like value objects, and any reader/writer or Factory classes) before designing around it.
  2. tests/ — the fastest way to see idiomatic construction and what output the authors assert on.
  3. README — usually the only docs you get.

Do not guess class names from another calendar library; confirm them against src/.

First use case: a downloadable .ics feed. This is the highest-leverage thing the package does for a typical Laravel app. A user clicks "Add to calendar" on an event page, or subscribes to /calendar.ics, and you hand them RFC 5545 output.

// app/Http/Controllers/EventCalendarController.php
public function __invoke(Event $event): Response
{
    $calendar = $this->calendarFactory->fromEvents(collect([$event]));

    return response($calendar->toString(), 200, [
        'Content-Type'        => 'text/calendar; charset=utf-8',
        'Content-Disposition' => 'attachment; filename="event-' . $event->id . '.ics"',
    ]);
}

Get that one endpoint working end-to-end (download → open in Google Calendar → open in Outlook) before building anything else on top of it. Calendar clients are unforgiving and the failure modes are invisible in unit tests.

Implementation Patterns

Wrap it behind a port of your own. This is the single most important pattern given the package's adoption profile:

interface CalendarExporter
{
    /** @param iterable<Event> $events */
    public function export(iterable $events): string;
}

final class EndroidCalendarExporter implements CalendarExporter { /* ... */ }

Bind it in a service provider. Every controller, mailable, and job then depends on your interface, not on Endroid\Calendar\*. Migrating to sabre/vobject or eluceo/ical later becomes a one-class change instead of a grep across the codebase.

Map Eloquent models to value objects, never pass models into the library. Keep a dedicated mapper so your schema can change independently of the calendar payload:

final class EventToCalendarItem
{
    public function map(Event $event): CalendarItem
    {
        return new CalendarItem(
            uid:         "event-{$event->id}@{$event->account_id}.example.com",
            summary:     $event->title,
            description: $event->description,
            startsAt:    $event->starts_at->copy()->utc(),
            endsAt:      $event->ends_at->copy()->utc(),
            allDay:      $event->is_all_day,
        );
    }
}

UIDs must be stable and globally unique. Derive them from your primary key plus a tenant/domain suffix. Unstable UIDs cause clients to duplicate events on every refresh of a subscribed feed.

Use it for two distinct flows. Export (user-initiated download, always fresh, Cache-Control: no-store) and subscribe (public, cacheable, long-lived URL, must be cheap). For the subscribe flow, generate into a cache keyed on a fingerprint of the underlying collection:

return Cache::remember("calendar.{$user->id}.v{$user->calendar_version}", 3600, fn () => $exporter->export($events));

Bump calendar_version on write so clients pick up changes without you dropping the cache manually.

Reading/importing is the riskier half. If the package has a parser, treat its input as hostile: cap upload size, reject on exception, and run the import inside a transaction or job. Validate that the parsed event count matches what you inserted before committing.

Attach to mailables for "you're registered" emails — generate the ICS in a queued job and attach it as a raw string, so the web request doesn't pay for it.

Test with a frozen clock. Carbon::setTestNow() in setUp() makes DTSTAMP/created fields deterministic and keeps assertions readable.

Gotchas and Tips

Line endings and folding will bite you. RFC 5545 requires CRLF and folds lines at 75 octets (not characters). If the library doesn't do this, Outlook silently rejects or truncates. Assert on raw bytes in a test: $this->assertStringContainsString("\r\n", $output) and check for \r\n continuation on a deliberately long DESCRIPTION.

Escaping. Commas, semicolons, backslashes, and newlines in SUMMARY/DESCRIPTION/LOCATION must be escaped per the spec. A user typing a comma in an event title is enough to corrupt the payload. If the library doesn't escape for you, escape in your mapper — do not assume.

All-day events are a different value type, not a midnight timestamp. DTSTART;VALUE=DATE:20240315 versus DTSTART:20240315T000000Z. Mixing these up produces off-by-one-day bugs for users east or west of UTC. Keep an explicit allDay flag on your value object.

Timezones. Prefer emitting UTC (Z suffix) and converting at the boundary. If you emit TZID= you inherit the client's VTIMEZONE resolution, which varies. Laravel stores UTC by default, so this is usually the path of least resistance — but always copy() the Carbon instance before ->utc(), since mutating a model's attribute is a classic quiet bug.

No auto-discovery, no config. Everything is hand-wired. That's fine, but put the factory construction in one place — a singleton binding — rather than new-ing it in controllers, so the product ID / domain / defaults are consistent across every feed you emit.

Debugging checklist when a feed "doesn't work": validate with an external RFC validator, then test in at least two of Google Calendar / Apple Calendar / Outlook — they diverge. Check Content-Type includes charset=utf-8, confirm the response body has no leading whitespace (a stray newline from a Blade template before <?php will break it), and confirm you're not gzipping in a way the client rejects.

Extension points are limited. With 0 dependents and low activity, assume no plugin surface and no responsive upstream. Fork-friendly license (MIT) means the realistic extension strategy is: wrap (interface), then fork or replace if you need RRULE recurrence, VALARM, attendees, or METHOD:REQUEST/iTIP scheduling. Basic event export is a small surface; recurring events and scheduling are not — if you need those, evaluate sabre/vobject up front rather than discovering the gap in month three.

Pin the version. With one maintainer and no downstream usage to catch regressions, use an exact constraint ("endroid/calendar": "1.2.*", or whatever the current line is) and upgrade deliberately after reading the diff. Renovate/Dependabot auto-merges on a package like this are how you get a Friday-afternoon outage.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings