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 Parser Laravel Package

johngrogg/ics-parser

A PHP library for parsing iCalendar (.ics) files into associative arrays of calendars and events, with DateTime support, time zone handling, and resolved property arrays. Install via Composer as johngrogg/ics-parser.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with the Composer name, not the GitHub name:

composer require johngrogg/ics-parser:^3

Then parse a calendar and dump what you get — the fastest way to learn the shape of the data is to look at a real event object:

use ICal\ICal;

$ical = new ICal(storage_path('app/calendars/team.ics'), [
    'defaultTimeZone' => config('app.timezone'),
    'defaultSpan'     => 1,   // years of recurrence generated when an RRULE has no end
]);

foreach ($ical->events() as $event) {
    dump($event->summary, $event->dtstart, $event->dtend, $event->uid);
}

First real use case: a public Google/Outlook .ics feed rendered as an "upcoming 7 days" list.

$ics = Http::timeout(10)->get($url)->body();

$ical = new ICal();
$ical->initString($ics);                    // avoids allow_url_fopen / redirect surprises

$upcoming = collect($ical->events())
    ->map(fn ($e) => [
        'title' => $e->summary ?? '(no title)',
        'start' => \Illuminate\Support\Carbon::parse($e->dtstart, config('app.timezone')),
        'end'   => isset($e->dtend) ? \Illuminate\Support\Carbon::parse($e->dtend, config('app.timezone')) : null,
        'where' => $e->location ?? null,
    ])
    ->filter(fn ($e) => $e['start']->betweenIncluded(now(), now()->addDays(7)))
    ->sortBy('start');

Where to look first:

  • The options array in the README (defaultSpan, defaultTimeZone, defaultWeekStart, filterDaysBefore, filterDaysAfter, skipRecurrence, disableCharacterReplacement, httpUserAgent) — that's the entire configuration surface.
  • examples/index.php in the repo — the canonical "render a month view" example and the best reference for the accessor style (getEventCount(), initEvent($i), getSummary(), getStartTime(), …).
  • Whatever dump($event) shows you for your own feed. Property sets vary by producer; real feeds differ from the RFC.

Implementation Patterns

Wrap it in one service, normalize immediately. Don't sprinkle new ICal(...) through controllers. Parse once, map to your own DTO/Eloquent model, and never let library objects escape:

class IcsFeedReader
{
    public function read(string $ics, array $options = []): array
    {
        $ical = new ICal(null, $options + [
            'defaultTimeZone' => config('app.timezone'),
            'defaultSpan'     => 1,
        ]);
        $ical->initString($ics);

        return array_map(function ($e) {
            return [
                'uid'     => $e->uid ?? null,
                'title'   => $e->summary ?? '',
                'start'   => isset($e->dtstart) ? Carbon::parse($e->dtstart) : null,
                'end'     => isset($e->dtend) ? Carbon::parse($e->dtend) : null,
                'all_day' => isset($e->dtstart) && !str_contains($e->dtstart, ':'),
                'location'=> $e->location ?? null,
                'notes'   => $e->description ?? null,
                'status'  => $e->status ?? null,
            ];
        }, $ical->events());
    }
}

Fetch with Laravel's HTTP client, parse with initString(). initUrl() relies on allow_url_fopen and gives you very little control (no auth headers, weak timeout/retry semantics). Pull the bytes with Http::withToken(...)->retry(3, 200)->get($url)->body() and hand the string to the parser. This also makes it trivial to cache the raw feed (Cache::remember("ics:{$feed->id}", 600, ...)) and to snapshot it to disk for debugging when a parse goes wrong.

Sync into your own table, keyed on uid. upsert() on (feed_id, uid) for non-recurring feeds; for recurring ones, key on (feed_id, uid, start_at) because expansion produces many instances sharing a UID. Use soft deletes for events that disappear from a feed, but only within the window you actually fetched — otherwise a narrow filterDaysAfter will wipe your whole future calendar.

Prefer the array API for bulk, the accessor API for single-event screens. $ical->events() returns everything at once (simple, memory-bound); getEventCount() + initEvent($i) + getSummary() / getStartTime() gives you a cursor-style read if you only need to render, say, one event detail page.

Run it in a queue, not a request. Parsing a few thousand events with recurrence expansion takes hundreds of milliseconds to seconds. A SyncCalendarFeed job on a schedule, writing to the DB, means your request path only ever reads your own models — and it gives you retries and failure alerting for free.

Test with fixture files, not the network. Commit three or four .ics files (single event, recurring with UNTIL, all-day, weird charset) and use initString(file_get_contents(...)) in PHPUnit/Pest. This is the single highest-value habit with this library, because recurrence edge cases are where it will surprise you.

Laravel-ish glue: bind the reader as a singleton in a service provider if you like, but keep the ICal instance itself short-lived — construct per parse. The parser keeps mutable state on the object ($this->event), so sharing one instance across jobs is a footgun.

Gotchas and Tips

  • Package name mismatch. composer require u01jmg3/ics-parser fails. The Packagist vendor is johngrogg. This wastes people's time constantly — put it in your README.
  • dtstart / dtend are strings, not DateTime objects. They come through in a Y-m-d H:i:s-ish shape resolved against defaultTimeZone. Always wrap with Carbon::parse() and ->setTimezone(config('app.timezone')) before storing or comparing. Never compare them as strings across feeds.
  • Properties may be absent. Feeds omit description, location, organizer, status, etc. Accessing $event->location on an event without one will warn under PHP 8. Use ?? null or isset() everywhere — especially in Blade.
  • No DTEND on all-day events. All-day events typically have a date-only DTSTART and either a DTEND of the next day (exclusive, per RFC) or nothing at all. Don't render "ends tomorrow" to users; treat the date as a single day.
  • DURATION instead of DTEND. Some producers emit DURATION:PT1H. Check $event->duration if dtend is missing and compute the end yourself.
  • Recurrence is the sharp edge. The parser expands RRULEs at parse time. Three consequences: (1) defaultSpan (default 2 years) bounds open-ended rules — lower it to 1 and you'll cut parse time noticeably; (2) skipRecurrence => true gives you only the master events, which is what you want if you plan to expand with rlanvin/php-rrule yourself; (3) support for exotic rules (BYSETPOS, mixed RDATE/EXDATE, RECURRENCE-ID overrides) is best-effort, not a full RFC 5545 engine. Verify against the specific calendar provider you integrate with.
  • Time zones need to be known. TZID values must be IANA, CLDR, or Windows names; a bespoke TZID (some Exchange exports) falls back to the default. Set defaultTimeZone explicitly and call $ical->setDefaultTimeZone('Europe/London') after construction if you need to override. Floating times (no Z, no TZID) are interpreted in the default zone — decide deliberately whether that's UTC or the user's zone.
  • initUrl() requires allow_url_fopen. Many hardened PHP images disable it. Another reason to download the feed yourself.
  • Charset weirdness. Outlook/Exchange feeds often contain Windows-1252 smart quotes and en-dashes. The library silently replaces a set of these; if you want raw bytes, set disableCharacterReplacement => true. Escape summary/description on output regardless — an ICS feed is untrusted input and can carry HTML/script payloads.
  • Memory scales with event count. events() materializes everything, and expanded recurrences multiply that. For big feeds, cap with filterDaysBefore / filterDaysAfter and defaultSpan, or skipRecurrence and expand lazily.
  • Exceptions are generic. You'll get \Exception with a message string (bad path, unreadable file, invalid week start). Wrap the parse in try/catch, log $e->getMessage() plus the raw feed body — without the body, debugging a malformed feed is guesswork.
  • It only reads. There's no writer. If you need to generate .ics, reach for eluceo/ical or spatie/icalendar-generator and keep this one for ingestion.
  • Extension points are thin. The clean seam is the options array plus post-parse normalization. Subclassing ICal to override date/duration formatting is possible but brittle across minor versions — prefer transforming after events() returns.
  • Validate the input. Run unfamiliar feeds through icalendar.org's validator once. If the validator flags it, expect the parser to drop properties silently rather than throw — a "missing location" bug is often a malformed LOCATION line upstream.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator