A lightweight and robust iCalendar (RFC 5545) parser for PHP. Documentation: ozzyczech.github.io/icalparser
- reads real-world
.icsfiles from Google Calendar, Apple Calendar, Outlook, Exchange, Nextcloud, Fastmail and others, repairs damaged files and reports every repair - keeps unknown and X- properties, writes calendars back
- creates calendars with named arguments (
Event::new(summary: ..., start: ...)), with VTIMEZONE definitions - keeps the meaning of dates, floating, UTC and zoned times, resolves Windows timezones and custom VTIMEZONE definitions
- expands recurring events lazily: the complete RRULE (including BYSETPOS and BYWEEKNO), RDATE, EXDATE and RECURRENCE-ID overrides (moved, cancelled and THISANDFUTURE)
- safe for untrusted input: strict and permissive mode, resource limits, streaming of large files
composer require om/icalparseruse om\ICal;
$calendar = ICal::parseFile('calendar.ics'); // or ICal::parse($content)
foreach ($calendar->events() as $event) {
echo $event->summary(), ' ', $event->start()?->format('Y-m-d H:i'), PHP_EOL;
}
// every instance of every event in a period, sorted, with moved and cancelled instances applied
$from = new DateTimeImmutable('2026-01-01');
$to = new DateTimeImmutable('2026-02-01');
foreach ($calendar->occurrencesBetween($from, $to) as $occurrence) {
// the local time; ->startTime($timezone) gives an instant (see "Dates and times")
printf("%s %s%s\n", $occurrence->start->format('j. n. H:i'), $occurrence->summary(), $occurrence->isModified() ? ' (changed)' : '');
}Events, tasks (todos()), journal entries (journals()) and free/busy components (freeBusy()) have typed getters:
uid(), summary(), description(), location(), start(), end(), duration(), status(), categories(),
organizer(), attendees(), alarms(), recurrenceRule(), color(), images() and more, see
reading calendars. Any property, including unknown ones, is available too:
$event->property('X-APPLE-STRUCTURED-LOCATION')?->parameter('X-TITLE');
$event->value('X-MICROSOFT-CDO-BUSYSTATUS'); // the typed valuestart(), end() and the occurrences return DateTimeValue, which keeps the difference between
20261010 (a date), 20261010T100000 (floating), 20261010T100000Z (UTC) and TZID=Europe/Prague:20261010T100000 (zoned).
Floating times and dates are never converted with the PHP default timezone:
$start = $event->start();
$start->format('Y-m-d H:i'); // the local value, always
$start->toDateTime(); // the instant; needs a timezone for dates and floating times
$start->toDateTime(new DateTimeZone('Europe/Prague')); // in a given timezone
$start->isDate(); $start->isFloating(); $start->isUtc(); $start->isZoned();The timezone of dates and floating times is X-WR-TIMEZONE of the calendar or the one configured with
ICal::parser()->floatingTimezone(...). See values and timezones.
foreach ($event->occurrencesBetween($from, $to) as $occurrence) { /* ... */ }
foreach ($event->occurrences(limit: 10) as $occurrence) { /* ... */ }Occurrences are generated lazily and only for a window or up to a limit; there is no unlimited expansion. The recurrence engine can be used on its own:
use om\RRule\Expander;
use om\RRule\Rule;
$rule = Rule::fromString('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1;COUNT=3'); // the last workday
foreach (new Expander($rule, new DateTimeImmutable('2026-01-30 09:00', new DateTimeZone('Europe/Prague'))) as $timestamp) {
echo date('Y-m-d', $timestamp), PHP_EOL;
}See recurrence.
use om\ICal\Parser\ParserMode;
$result = ICal::parser()->mode(ParserMode::Permissive)->parseFile('feed.ics');
$calendar = $result->calendar();
foreach ($result->warnings() as $warning) {
echo $warning, PHP_EOL; // line 12: END:VEVENT is missing, the component was closed. [syntax.missing-end]
}The permissive mode (default) repairs damaged files and reports each repair as a warning. The strict mode throws an exception with an error code, line, property and raw value. Limits of the input and of recurrence expansion protect against pathological files. See parsing, warnings and limits and validation.
foreach (ICal::stream('huge.ics') as $item) { // events, tasks, ... one by one, constant memory
echo $item->summary(), PHP_EOL;
}use om\ICal\Alarm;
use om\ICal\Calendar;
use om\ICal\Event;
use om\ICal\Value\CalAddress;
$calendar = Calendar::create('-//example//standup//EN', name: 'Team A', events: [
Event::new(
summary: 'Standup, team A',
start: new DateTimeImmutable('2026-01-05 09:30', new DateTimeZone('Europe/Prague')),
duration: new DateInterval('PT15M'),
rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR',
attendees: [CalAddress::create('mailto:a@example.org', name: 'A', rsvp: true)],
alarms: [Alarm::display('Standup', trigger: '-PT5M')],
),
]);
echo $calendar->serialize(); // or $calendar->writeFile('team.ics')Values are formatted and escaped, UID, DTSTAMP and a VTIMEZONE for every timezone used are added, invalid combinations are rejected. Parsed calendars are serialized with all their properties, lines are folded at 75 octets. See creating calendars.
Recipes show common tasks: reading a feed from a URL, events in the viewer's timezone, JSON for a web calendar, a filtered copy, a subscription feed, e-mail invitations and checking uploads.
The examples directory contains a web page listing upcoming events of a sample calendar
(php -S localhost:8000 -t examples) and command line scripts for streaming, validation and writing.
The array based IcalParser of version 4 is still available and deprecated (it will be removed in 5.5 at the latest);
it keeps its output and fixes many bugs.
See UPGRADING.md and CHANGELOG.md.
iCal parser uses Nette Tester, PHPStan and PHP CS Fixer.
composer install
composer test # unit tests and tests of the version 4 API
composer test:integration # public API, parser modes, golden files of tests/Fixtures
composer test:fuzz # corrupted and pathological input
composer test:differential # comparison with python-dateutil, see below
composer analyse # PHPStan
composer cs # coding standard (cs:fix fixes it)
composer check # all of the above except differential testsThe differential test needs Python with dateutil and is skipped without it:
python3 -m venv .venv && .venv/bin/pip install python-dateutil, then run it with ICALPARSER_PYTHON=.venv/bin/python.
The documentation at ozzyczech.github.io/icalparser is built by
Starlight from docs/*.md, UPGRADING.md and CHANGELOG.md, and by
ApiGen from the docblocks of src/ (.github/workflows/docs.yml). Preview it with
composer create-project apigen/apigen:dev-master ../apigen --no-dev && php ../apigen/bin/apigen, then
cd website && npm install && npm run dev.
Every calendar in tests/Fixtures has a golden file with the normalized output. After an intended change,
regenerate them with UPDATE_SNAPSHOTS=1 composer test:integration and review the diff. Every bug gets a fixture
in tests/Fixtures/Regression or a test.