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

Ics Bundle Laravel Package

jsvrcek/ics-bundle

A Symfony bundle for generating iCalendar (.ics) files, letting you build events, calendars, and reminders from your application's data and export them for import into Outlook, Google Calendar, and other calendaring tools.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and register.

composer require jsvrcek/ics-bundle

The bundle predates Symfony Flex, so there is usually no recipe. Register it manually:

// Symfony 3/4: app/AppKernel.php
$bundles = [ /* ... */ new Jsvrcek\ICSBundle\JsvrcekICSBundle() ];

Check the actual namespace in vendor/jsvrcek/ics-bundle/ before pasting — the bundle's own class name is the first thing to confirm, since docs for this package are thin.

2. Find the service, not the config.

The bundle's real value is that it wires the Jsvrcek\ICS library into the container. Before writing anything, run:

bin/console debug:container ics

You are looking for the export service (the library's Jsvrcek\ICS\CalendarExport). The bundle also exposes a config tree under jsvrcek_ics: that lets you declare calendars and events statically in YAML — useful to confirm the wiring works, but almost never what you want in production, since it hard-codes dates into config.

3. First use case: a subscribable feed.

The canonical day-one task is "give me a URL I can paste into Google Calendar." That means one controller action that builds a Jsvrcek\ICS\Model\Calendar, attaches events, and returns an iCal response:

public function feedAction(CalendarExport $export): Response
{
    $calendar = new Calendar();
    $calendar->setProdId('-//Acme//Bookings//EN');
    $calendar->setTimezone(new \DateTimeZone('Europe/Berlin'));

    $event = (new CalendarEvent())
        ->setUid('[email protected]')
        ->setStart(new \DateTime('2024-06-01 09:00', new \DateTimeZone('Europe/Berlin')))
        ->setEnd(new \DateTime('2024-06-01 10:00', new \DateTimeZone('Europe/Berlin')))
        ->setSummary('Kickoff')
        ->setDescription('Project kickoff meeting');

    $calendar->addEvent($event);
    $export->addCalendar($calendar);

    $response = new Response($export->getStream(), 200, [
        'Content-Type' => 'text/calendar; charset=utf-8',
        'Content-Disposition' => 'inline; filename="acme.ics"',
    ]);

    return $response;
}

Route it with a .ics requirement (/feed/{token}.ics) and you have something every calendar client understands in about ten lines.

Implementation Patterns

Build in a service, not a controller. Calendar assembly is domain logic. Put a CalendarBuilder (or per-feed builder) in src/, have it accept your entities and return a populated Calendar. Controllers stay three lines and the builder becomes unit-testable — you can assert on the generated stream without booting HTTP.

One service per feed, dispatched by token. Auth for iCal feeds cannot use sessions or CSRF: Google Calendar, Outlook, and Apple Calendar fetch the URL server-side with no cookies. The pattern that works is an unguessable token in the path or query string, resolved to a user/tenant by a listener, then passed to the builder. Treat that token like a password — it is a bearer credential and it will end up in logs and screenshots.

Cache the rendered stream, not the objects. Feed clients poll aggressively (Google as often as every few minutes). Generate the string once, store it under a key derived from the user and their last-modified timestamp, and serve from cache. Regenerate on writes to the underlying entities. This is the single biggest day-to-day win — a feed hitting the database on every poll is a slow-motion outage.

Return 304 when nothing changed. Compute an ETag/Last-Modified from the same version marker used for the cache key and short-circuit. Symfonys Response::isNotModified() handles the headers.

Use stable UIDs. setUid('booking-' . $entity->getId() . '@acme.example') — if the UID changes between polls, clients create duplicate events instead of updating the existing one. This is the most common "why are there 400 copies of my meeting" bug with hand-rolled iCal.

Bump DTSTAMP/LAST-MODIFIED on edit. Clients use these, not your HTTP headers, to decide whether an event changed. If you generate them from new \DateTime() on every request, every poll looks like a change and clients re-process the whole feed; if you never change them, edits are silently ignored. Derive them from the entity's updatedAt.

Recurrence and alarms. The library supports RRULE via the recurrence model and VALARM via CalendarAlarm attached to an event. For anything beyond a simple weekly repeat, generate the RRULE string once in a domain object and test it against a real client — RRULE correctness is not something unit tests catch well.

Multiple calendars in one file. CalendarExport::addCalendar() can take several Calendar objects; useful for exporting "my bookings" plus "team holidays" as a single subscription. Keep the PRODID consistent across requests or some clients treat it as a new feed.

Symphony version reality check. This bundle targets Symfony 2/3. On Symfony 5+ the bundle registration and DI config will likely not load cleanly. The pragmatic pattern is to skip the bundle entirely: register CalendarExport as a plain service in services.yaml and use the underlying jsvrcek/ics library directly. You lose the config tree (which you weren't using) and keep the actual functionality.

Gotchas and Tips

Whitespace before <?php or after ?>. Any stray byte prepended to the output — a BOM, a blank line in a config file, a stray echo in a bundle — lands before BEGIN:VCALENDAR and Google rejects the feed with an unhelpful "could not read" error. If a feed suddenly stops working, curl -sI and curl -s | xxd | head are the first things to run.

Line endings must be CRLF. The library folds long lines at 75 octets with CRLF continuations. If you pipe the stream through something that normalizes newlines (a formatter, a middleware, file_put_contents with translation), the feed breaks. Serve the raw string.

Timezones are a two-part deal. Setting TZID on a datetime without a matching VTIMEZONE component in the calendar produces a file that Outlook reads as UTC and Google reads as floating. Either let the library emit the VTIMEZONE by setting the calendar's timezone, or use UTC Z timestamps everywhere and skip TZIDs entirely. The second option is boring and never fails.

DTEND is exclusive for all-day events. A one-day event on June 1st needs DTSTART;VALUE=DATE:20240601 and DTEND;VALUE=DATE:20240602. Setting both to the same day yields a zero-length event that some clients drop silently.

Escaping. Commas, semicolons, backslashes, and literal newlines in SUMMARY/DESCRIPTION/LOCATION must be escaped (\, \; \\ \n). The library escapes standard fields, but custom X- properties and anything you concatenate yourself bypass that. User-supplied event titles containing a comma are the classic failure.

UTF-8 folding bug. Older releases of the underlying library fold at 75 bytes, which can split a multi-byte character in half and produce mojibake in descriptions with emoji or non-Latin text. If you see é in calendar clients, this is why — pin a library version where it is fixed or avoid long non-ASCII descriptions.

Duplicate UIDs kill the whole calendar. One duplicate UID and some clients (notably Google) reject the entire feed rather than the one event. Assert uniqueness in the builder before export.

Content-Type must be text/calendar. If Symfony's Response gets the charset or type wrong, browsers render the file as text and clients refuse it. Set the header explicitly rather than relying on a default.

Testing. Don't assert on the raw string — it contains DTSTAMPs. Unfold the output (join lines starting with a space), assert on the presence of SUMMARY:/UID: lines, and snapshot-test the folded structure separately with DTSTAMP normalized.

Maintenance risk. Eleven stars and zero dependents means you are effectively the maintainer. Budget for vendoring the underlying library or forking the bundle if you build anything long-lived on it, and prefer calling the library directly so the bundle's Symfony-version coupling never becomes your problem.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource