welp/ical-bundle
Symfony bundle for working with iCalendar (ICS) files: parse and generate .ics calendars and events, and integrate calendar data into your Symfony applications.
The package is a Symfony bundle, not a Laravel package, so there is no IcalServiceProvider or facade to lean on. Plan to spend the first hour reading src/ and Resources/config/services.yaml rather than the README — the bundle's behaviour lives almost entirely in those two places.
composer require welp/ical-bundle. Because it targets an older Symfony (pre-5.x era, last tagged 2020-03), expect Composer to complain about symfony/* version constraints. You will almost certainly need to add --with-all-dependencies or pin the underlying iCal library version that the bundle wraps.bundles.php. Instead, register the bundle's services manually in a dedicated service provider (see Implementation Patterns), or — the pragmatic route — bypass the bundle entirely and depend on whatever iCal library it pulls in (typically an eluceo/ical-style builder) and use it directly. Given 10 stars and zero dependents, the bundle's wrapper adds little value over the library it decorates..ics payload for a single calendar event and returning it as a download response. This exercises serialisation, timezone handling, and the response path in one go:$event = [
'start' => new \DateTimeImmutable('2026-06-01 10:00', new \DateTimeZone('Europe/Berlin')),
'end' => new \DateTimeImmutable('2026-06-01 11:00', new \DateTimeZone('Europe/Berlin')),
'summary' => 'Kickoff',
'location' => 'Room 4',
];
return response($icalService->render($event), 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="event.ics"',
]);
If you want the bundle's builder rather than the raw library, register its services in a provider and expose a facade:
public function register(): void
{
$this->app->singleton(\Welp\IcalBundle\Service\IcalService::class, function ($app) {
return new \Welp\IcalBundle\Service\IcalService(
// whatever the bundle's services.yaml passes in — read it first
);
});
}
Bind it as a singleton so repeated event additions within one request accumulate into a single VCALENDAR body. A fresh instance per event produces multiple VCALENDAR blocks glued together, which most calendar clients reject.
The most natural day-to-day integration is a subscribable feed, not a one-off download. Model it as a controller action that queries Eloquent, maps rows to events, and streams the result:
public function feed(User $user): Response
{
$events = $user->events()->where('starts_at', '>', now())->get();
return response($this->ical->renderCollection($events), 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
])->setCache(['public' => 60, 'max_age' => 600]);
}
Apple Calendar and Google Calendar poll these URLs; caching headers matter more than you'd think, and conditional GETs (ETag/Last-Modified) cut load noticeably.
For invitations, generate the payload and attach it via Laravel's mailer:
$mail->attachData($ical->render($event), 'invite.ics', ['mime' => 'text/calendar']);
Set the METHOD:REQUEST property (if the underlying library exposes it) so Outlook treats it as a meeting invite rather than a file. Without it, invitations show as plain attachments.
Keep Eloquent models out of the calendar builder. Map them into a small CalendarEvent value object (start, end, UID, summary, description, location, status, sequence). This gives you one place to enforce the two rules that bite everyone: UID stability (derive from "event-{$model->id}@yourdomain", never regenerate per request) and SEQUENCE increments on updates so subscribed clients refresh rather than ignore.
spatie/icalendar-generator, eluceo/ical) and save yourself the bridge work.Resources/config/services.yaml and may reference Symfony-only services (RequestStack, EventDispatcherInterface, translator). Anything it depends on that Laravel doesn't bind will throw at resolution time. Read the YAML before registering anything.DateTime objects without an explicit DateTimeZone inherit PHP's default, which may differ from your app timezone. Always construct with an explicit zone, and if events are in floating/local time, ensure the library emits DTSTART;TZID=... rather than a UTC Z value. A one-hour drift in a feed is almost always this.\r\n the content and don't fold long DESCRIPTION fields, producing files that parse in one client but not another. If you hand-roll anything, test with a long description.SUMMARY/DESCRIPTION must be escaped (\,, \;, \\, \n). User-entered event titles are the usual source of corrupt .ics files.ical: { ... }) has no Laravel equivalent. If the bundle reads kernel parameters or %env()% placeholders, replicate those via config/services.php and your own Config access, or hardcode values in the bridge provider.VCALENDAR string. For thousands, chunk and echo with ob_flush() on a StreamedResponse.X--prefixed properties or VALARM blocks, you'll subclass the builder or post-process the serialized output with regex — not elegant, so wrap it in a small adapter you control so you can swap the library later.How can I help you explore Laravel packages today?