Skip to content

Repository files navigation

Packagist Version Packagist Downloads Packagist License Packagist PHP Version PHP Tests

PHP iCal Parser

A lightweight and robust iCalendar (RFC 5545) parser for PHP. Documentation: ozzyczech.github.io/icalparser

  • reads real-world .ics files 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

Install

composer require om/icalparser

Usage

use 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 value

Dates and times

start(), 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.

Recurring events

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.

Strict and permissive parsing

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.

Large files

foreach (ICal::stream('huge.ics') as $item) { // events, tasks, ... one by one, constant memory
	echo $item->summary(), PHP_EOL;
}

Creating calendars

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.

Examples

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.

Upgrading from version 4

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.

Development

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 tests

The 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.

About

Simple iCal parser for PHP for parsing format into array

Topics

Resources

Stars

61 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages