PHPackages                             erenav/icalendar - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. erenav/icalendar

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

erenav/icalendar
================

A modern, strongly-typed, immutable iCalendar (RFC 5545 / 7986 / 5546) library for PHP.

v0.4.0(today)00MITPHPPHP &gt;=8.3CI passing

Since Jun 20Pushed todayCompare

[ Source](https://github.com/erenav/icalendar)[ Packagist](https://packagist.org/packages/erenav/icalendar)[ Docs](https://github.com/vanere/icalendar)[ RSS](/packages/erenav-icalendar/feed)WikiDiscussions main Synced today

READMEChangelog (3)Dependencies (4)Versions (4)Used By (0)

erenav/icalendar
================

[](#erenavicalendar)

[![Latest Version](https://camo.githubusercontent.com/887ceda3d70508e4314aea65ef2dfb265505317c270a71c9d38dc6dafd8adb56/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6572656e61762f6963616c656e6461722e737667)](https://packagist.org/packages/erenav/icalendar)[![Tests](https://github.com/erenav/icalendar/actions/workflows/ci.yml/badge.svg)](https://github.com/erenav/icalendar/actions/workflows/ci.yml)[![PHP Version](https://camo.githubusercontent.com/c012e265153f1daf67fda880584d30c69fda69d32ae41ce7ddd2475424b8f128/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6572656e61762f6963616c656e6461722e737667)](https://packagist.org/packages/erenav/icalendar)[![Total Downloads](https://camo.githubusercontent.com/1ab221ca3a838d772921e316c2c683a92ee75207939520d5c3371efcf204df4c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6572656e61762f6963616c656e6461722e737667)](https://packagist.org/packages/erenav/icalendar)[![License](https://camo.githubusercontent.com/09aec5c29e2009135a61940c0c88a3b5d550898a049b88d958d35c8da6ac83c4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6572656e61762f6963616c656e6461722e737667)](LICENSE)

A modern, strongly-typed, **immutable** iCalendar library for PHP 8.3+.

Implements [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545) (iCalendar), [RFC 7986](https://www.rfc-editor.org/rfc/rfc7986) (new properties), and [RFC 5546](https://www.rfc-editor.org/rfc/rfc5546) (iTIP scheduling).

No stringly-typed array access, no `$event['VEVENT']['SUMMARY']`. Fluent builders, immutable value objects, typed getters, and **lossless round-tripping** of anything the library doesn't model.

```
use Erenav\ICalendar\Component\{Calendar, Event};
use Erenav\ICalendar\Serializer\IcsSerializer;
use Erenav\ICalendar\ValueType\Duration;

$calendar = Calendar::build()
    ->prodId('-//Acme//Booking 1.0//EN')
    ->add(
        Event::build()
            ->uid('booking-42@acme.test')
            ->summary('Sprint Planning')
            ->starts(new DateTimeImmutable('2026-07-01 10:00', new DateTimeZone('UTC')))
            ->lasting(Duration::hours(1))
            ->addAttendee('alice@acme.test')
    )
    ->get();

echo (new IcsSerializer)->serialize($calendar);
```

---

> 📖 **New here?** The [Recipes](docs/RECIPES.md) page has short, copy-paste examples for the most common tasks — start there.

Table of contents
-----------------

[](#table-of-contents)

- [Why this library](#why-this-library)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Building calendars](#building-calendars)
- [Serializing to `.ics`](#serializing-to-ics)
- [Parsing `.ics`](#parsing-ics)
- [Reading data](#reading-data)
- [Editing immutably](#editing-immutably)
- [Dates, times &amp; time zones](#dates-times--time-zones)
- [Durations](#durations)
- [Attendees &amp; organizer](#attendees--organizer)
- [Alarms](#alarms)
- [Recurring events](#recurring-events)
- [Time zones](#time-zones)
- [Scheduling (iTIP)](#scheduling-itip)
- [Custom &amp; unknown properties](#custom--unknown-properties)
- [Strict vs lenient](#strict-vs-lenient)
- [Error handling](#error-handling)
- [Gotchas &amp; current limitations](#gotchas--current-limitations)
- [Architecture](#architecture)
- [Testing](#testing)
- [Roadmap](#roadmap)
- [License](#license)

---

Why this library
----------------

[](#why-this-library)

[`sabre/vobject`](https://github.com/sabre-io/vobject) is the established option, but it leans on stringly-typed array access and mutable objects. `erenav/icalendar` aims for:

- **Strong typing** — enums for parameters/statuses, dedicated value objects for dates, durations, periods, geo, etc. Illegal states are unconstructable.
- **Immutability** — every component and value is `readonly`. You mutate through a builder and get a fresh object.
- **Fluent construction** — `Event::build()->summary(...)->addAttendee(...)->get()`.
- **Lossless round-trips** — properties and components it doesn't model are preserved verbatim, so reading and re-writing a third-party `.ics` never silently drops data.
- **Zero runtime dependencies.**

Requirements
------------

[](#requirements)

- PHP **8.3+**
- No runtime dependencies (the recurrence engine in phase 2 will add `rlanvin/php-rrule`)

Installation
------------

[](#installation)

```
composer require erenav/icalendar
```

Quick start
-----------

[](#quick-start)

```
require 'vendor/autoload.php';

use Erenav\ICalendar\Component\{Calendar, Event};
use Erenav\ICalendar\Parser\Parser;
use Erenav\ICalendar\Serializer\IcsSerializer;
use Erenav\ICalendar\ValueType\Duration;

// Build
$calendar = Calendar::build()
    ->prodId('-//Acme//EN')
    ->add(
        Event::build()
            ->uid('1@acme.test')
            ->summary('Lunch')
            ->starts(new DateTimeImmutable('2026-07-01 12:00', new DateTimeZone('UTC')))
            ->lasting(Duration::hours(1))
    )
    ->get();

// Serialize
$ics = (new IcsSerializer)->serialize($calendar);

// Parse back
$parsed = Parser::lenient()->parseCalendar($ics);
echo $parsed->events()[0]->summary(); // "Lunch"
```

Building calendars
------------------

[](#building-calendars)

`Calendar::build()`, `Event::build()` and `Alarm::build()` return **mutable builders**. Calling `->get()` produces the **immutable** component.

```
use Erenav\ICalendar\Component\{Calendar, Event};
use Erenav\ICalendar\Parameter\{Role, PartStat};
use Erenav\ICalendar\Property\{EventStatus, Transparency, Classification};
use Erenav\ICalendar\ValueType\Duration;

$event = Event::build()
    ->uid('meeting-42@acme.test')
    ->timestamp(new DateTimeImmutable('now', new DateTimeZone('UTC'))) // DTSTAMP
    ->summary('Sprint Planning, Q3')          // commas/semicolons escaped automatically
    ->description("Agenda:\n- demo\n- retro")
    ->location('Room 4')
    ->url('https://acme.test/meetings/42')
    ->starts(new DateTimeImmutable('2026-07-01 09:30', new DateTimeZone('UTC')))
    ->lasting(Duration::hours(1))             // or ->ends($dateTime)
    ->status(EventStatus::Confirmed)
    ->transparency(Transparency::Opaque)
    ->classification(Classification::Private)
    ->priority(5)
    ->categories('work', 'planning')
    ->color('cornflowerblue')                 // RFC 7986
    ->organizer('boss@acme.test', name: 'The Boss')
    ->addAttendee('alice@acme.test', role: Role::Chair, rsvp: true, name: 'Alice')
    ->addAttendee('bob@acme.test', partStat: PartStat::Accepted)
    ->get();

$calendar = Calendar::build()
    ->prodId('-//Acme//Booking 1.0//EN')      // VERSION defaults to 2.0
    ->name('Team Calendar')                   // RFC 7986
    ->add($event)                             // accepts components or builders
    ->get();
```

Serializing to `.ics`
---------------------

[](#serializing-to-ics)

```
use Erenav\ICalendar\Serializer\IcsSerializer;

$ics = (new IcsSerializer)->serialize($calendar);

// Strict mode validates required properties (UID, DTSTAMP, VERSION, PRODID, …)
$ics = (new IcsSerializer(strict: true))->serialize($calendar);
```

The serializer handles CRLF line endings, 75-octet line folding (UTF-8 safe), TEXT escaping, RFC 6868 parameter encoding, and derives `TZID` / `VALUE` / `ENCODING`parameters from the values themselves.

Parsing `.ics`
--------------

[](#parsing-ics)

```
use Erenav\ICalendar\Parser\Parser;

$calendar = Parser::lenient()->parseCalendar($icsString); // returns Calendar
$component = Parser::lenient()->parse($icsString);         // returns the root Component

// Strict parsing throws on RFC violations instead of recovering:
$calendar = Parser::strict()->parseCalendar($icsString);
```

Parsing is **lossless (Level-1)**: unknown properties, unknown components, and unrecognized parameter values are preserved, so `serialize(parse($ics))` round-trips without dropping data (see [Gotchas](#gotchas--current-limitations) for what "Level-1" means exactly).

Reading data
------------

[](#reading-data)

Typed getters read from the underlying model. Optional properties return `null`.

```
$event = $calendar->events()[0];

$event->uid();            // ?string
$event->summary();        // ?string  (already unescaped)
$event->description();    // ?string
$event->location();       // ?string
$event->start();          // ?DateTimeValue
$event->end();            // ?DateTimeValue  (computed from DTSTART+DURATION if no DTEND)
$event->duration();       // ?Duration
$event->status();         // ?EventStatus
$event->priority();       // ?int
$event->categories();     // list
$event->organizer();      // ?Organizer
$event->attendees();      // list
$event->alarms();         // list

// Calendar level
$calendar->productId();   // ?string
$calendar->version();     // ?string
$calendar->events();      // list
$calendar->components();  // list  (events, time zones, todos, …)
```

Anything without a dedicated getter is still reachable:

```
$event->property('X-APPLE-TRAVEL-ADVISORY-BEHAVIOR')?->value()->toString();
$event->hasProperty('RRULE');
foreach ($event->properties as $property) { /* … */ }
```

Editing immutably
-----------------

[](#editing-immutably)

Components are `readonly`. To change one, get a builder back, tweak it, and rebuild — the original is untouched.

```
$updated = $event->toBuilder()
    ->summary('Sprint Planning (rescheduled)')
    ->starts(new DateTimeImmutable('2026-07-02 09:30', new DateTimeZone('UTC')))
    ->get();

$event->summary();   // unchanged — original is immutable
$updated->summary(); // "Sprint Planning (rescheduled)"
```

Dates, times &amp; time zones
-----------------------------

[](#dates-times--time-zones)

iCalendar distinguishes four date/time forms. The `DateTimeValue` value object models all of them, and is the single source of truth for the `TZID` / `VALUE=DATE` parameters.

```
use Erenav\ICalendar\ValueType\DateTimeValue;

DateTimeValue::utc(new DateTimeImmutable('2026-07-01 10:00', new DateTimeZone('UTC')));
//   → 20260701T100000Z

DateTimeValue::zoned(new DateTimeImmutable('2026-07-01 09:30'), 'America/New_York');
//   → DTSTART;TZID=America/New_York:20260701T093000

DateTimeValue::floating(new DateTimeImmutable('2026-07-01 09:30'));
//   → 20260701T093000   (no zone, "local" time)

DateTimeValue::date(new DateTimeImmutable('2026-07-01'));
//   → DTSTART;VALUE=DATE:20260701   (all-day)
```

The builder's date setters accept **any `DateTimeInterface`** (so Carbon works), or a `DateTimeValue` when you need an explicit form:

```
$event = Event::build()
    ->starts($carbonInstance)                                  // inferred form
    ->ends(DateTimeValue::zoned($dt, 'Europe/Paris'))          // explicit form
    ->get();

// All-day event:
$allDay = Event::build()
    ->starts(DateTimeValue::date(new DateTimeImmutable('2026-07-01')))
    ->ends(DateTimeValue::date(new DateTimeImmutable('2026-07-02')))
    ->get();
```

Durations
---------

[](#durations)

`Duration` is a dedicated value object (not `DateInterval`) because the iCalendar `DURATION` type forbids months/years, has a distinct week form, and must be immutable. It bridges to native PHP both ways:

```
use Erenav\ICalendar\ValueType\Duration;

Duration::hours(1);                 // PT1H
Duration::minutes(-15);             // -PT15M  (negative — e.g. an alarm trigger)
Duration::weeks(2);                 // P2W
Duration::of(days: 1, hours: 6);    // P1DT6H
Duration::parse('PT90M');           // from a string

// Interop (CarbonInterval extends DateInterval, so it works too):
Duration::fromDateInterval(new DateInterval('PT1H'));
Duration::hours(1)->toDateInterval();
```

Attendees &amp; organizer
-------------------------

[](#attendees--organizer)

`addAttendee()` builds the `ATTENDEE` property and its parameters. `attendees()` returns the raw `Property` objects (lossless — you get the address *and* all parameters).

```
use Erenav\ICalendar\Parameter\{Role, PartStat, CuType};

$event = Event::build()
    ->organizer('boss@acme.test', name: 'The Boss', sentBy: 'mailto:assistant@acme.test')
    ->addAttendee('alice@acme.test', role: Role::Chair, partStat: PartStat::Accepted, rsvp: true, name: 'Alice')
    ->addAttendee('room-a@acme.test', cuType: CuType::Room)
    ->get();

$attendee = $event->attendees()[0];     // a typed Attendee
$attendee->address()->toString();      // "mailto:alice@acme.test"
$attendee->email();                    // "alice@acme.test"
$attendee->role();                     // Role::Chair       (typed)
$attendee->participationStatus();      // PartStat::Accepted
$attendee->commonName();               // "Alice"
$attendee->rsvp();                     // true
$attendee->property;                   // the underlying Property (lossless escape hatch)
```

Alarms
------

[](#alarms)

```
use Erenav\ICalendar\Component\{Event, Alarm};
use Erenav\ICalendar\Property\AlarmAction;
use Erenav\ICalendar\ValueType\Duration;

$event = Event::build()
    ->uid('1@acme.test')
    ->addAlarm(
        Alarm::build()
            ->action(AlarmAction::Display)
            ->description('Starts in 15 minutes')
            ->trigger(Duration::minutes(-15))   // relative; or pass a DateTimeInterface for absolute
    )
    ->get();

$event->alarms()[0]->action();   // AlarmAction::Display
$event->alarms()[0]->trigger();  // Duration (or DateTimeValue)
```

Recurring events
----------------

[](#recurring-events)

Recurrence rules are modelled by the immutable `Recurrence` value object and built fluently (each modifier returns a new instance):

```
use Erenav\ICalendar\Recurrence\{Recurrence, Weekday, WeekdayRule};

Recurrence::daily()->times(10);                          // FREQ=DAILY;COUNT=10
Recurrence::weekly()->every(2)->on(Weekday::Monday, Weekday::Wednesday);
Recurrence::monthly()->on(new WeekdayRule(Weekday::Friday, -1)); // last Friday of the month
Recurrence::yearly()->until(new DateTimeImmutable('2030-01-01', new DateTimeZone('UTC')));
Recurrence::parse('FREQ=WEEKLY;BYDAY=MO,WE');           // from an RRULE string
```

Attach one to an event, with optional exception (`EXDATE`) and extra (`RDATE`) dates:

```
use Erenav\ICalendar\ValueType\DateTimeValue;

$event = Event::build()
    ->uid('standup@acme.test')
    ->starts(DateTimeValue::zoned(new DateTimeImmutable('2026-07-01 09:30'), 'America/New_York'))
    ->recurrence(Recurrence::weekly()->on(Weekday::Monday, Weekday::Wednesday))
    ->addExceptionDate(new DateTimeImmutable('2026-12-25 09:30', new DateTimeZone('America/New_York')))
    ->get();

$event->isRecurring();     // true
$event->recurrenceRule();  // ?Recurrence
```

Expand the concrete occurrences in a window (`RRULE` + `RDATE` − `EXDATE`, DST-aware for IANA zones — wall-clock time is preserved across transitions):

```
$from = new DateTimeImmutable('2026-07-01');
$to   = new DateTimeImmutable('2026-08-01');

foreach ($event->occurrencesBetween($from, $to) as $occurrence) {
    echo $occurrence->format('Y-m-d H:i'); // DateTimeImmutable
}
```

Expansion wraps [`rlanvin/php-rrule`](https://github.com/rlanvin/php-rrule) behind a `RecurrenceExpander` interface — pass your own implementation to `occurrencesBetween()`to swap the engine.

### Modified &amp; cancelled instances (`RECURRENCE-ID`)

[](#modified--cancelled-instances-recurrence-id)

A recurring series can have individual instances overridden by a second `VEVENT` with the same `UID` plus a `RECURRENCE-ID`. Expand at the **calendar** level to resolve those — `Calendar::occurrencesBetween()` returns rich `Occurrence` objects (the effective event per instance), applying modifications and dropping cancellations:

```
foreach ($calendar->occurrencesBetween($from, $to) as $occurrence) {
    $occurrence->start;        // DateTimeImmutable (may differ from the slot if moved)
    $occurrence->recurrenceId; // the original slot in the series
    $occurrence->event;        // the master, or the override VEVENT for this instance
    $occurrence->isOverride;   // true if a RECURRENCE-ID override applied
}
```

(`Event::occurrencesBetween()` expands a single event and returns bare instants; `Calendar::occurrencesBetween()` is the override-aware version across the whole calendar.)

Time zones
----------

[](#time-zones)

Zoned date-times reference a `TZID`. For portability, a calendar can carry its own `VTIMEZONE` definitions so clients don't need to know the zone. `withTimeZones()` generates them automatically from PHP's tz database for every IANA zone your events use:

```
$calendar = Calendar::build()
    ->prodId('-//Acme//EN')
    ->add(
        Event::build()->uid('1@acme')
            ->starts(DateTimeValue::zoned(new DateTimeImmutable('2026-07-01 09:30'), 'America/New_York')),
    )
    ->get()
    ->withTimeZones(); // prepends a correct VTIMEZONE with STANDARD/DAYLIGHT + RRULEs

$calendar->timeZones();          // list
$calendar->timeZones()[0]->tzid(); // "America/New_York"
```

Parsed `VTIMEZONE` blocks are first-class `TimeZone` components with typed `Observance`children:

```
$tz = $calendar->timeZones()[0];
foreach ($tz->observances() as $observance) {
    $observance->isDaylight();        // bool
    $observance->offsetTo();          // ?UtcOffset
    $observance->recurrenceRule();    // ?Recurrence
}
```

You can also generate one directly: `(new TimeZoneGenerator())->forIana('Europe/Paris')`.

Scheduling (iTIP)
-----------------

[](#scheduling-itip)

Build [RFC 5546](https://www.rfc-editor.org/rfc/rfc5546) scheduling messages — invitations, replies, cancellations — each with the correct `METHOD` and required properties, via `ITip`:

```
use Erenav\ICalendar\Scheduling\{ITip, ITipValidator};
use Erenav\ICalendar\Parameter\PartStat;

$request = ITip::request($event);                                      // organizer invites attendees
$reply   = ITip::reply($event, 'alice@acme.test', PartStat::Accepted); // attendee responds
$cancel  = ITip::cancel($event);                                       // + STATUS:CANCELLED, SEQUENCE++
$publish = ITip::publish([$eventA, $eventB]);                          // a non-interactive feed

$request->schedulingMethod(); // Method::Request   (typed METHOD getter)
```

Validate a message against its method's constraints:

```
$validator = new ITipValidator();

$validator->isValid($request);     // bool
$validator->validate($request);    // list of problems (empty = valid)
$validator->assertValid($request); // throws SchedulingException if invalid
```

In the [Laravel package](https://github.com/erenav/laravel-icalendar), attaching an iTIP calendar advertises the method in the MIME type (`text/calendar; method=REQUEST`), so mail clients treat it as an invitation.

Custom &amp; unknown properties
-------------------------------

[](#custom--unknown-properties)

Add arbitrary properties with `->property()` (it appends, so it can repeat):

```
$event = Event::build()
    ->uid('1@acme.test')
    ->property('X-ACME-ROOM-ID', '4')
    ->get();
```

When **parsing**, anything not modelled is preserved verbatim as a `RawValue` (and unknown components become a `GenericComponent`), then re-emitted unchanged:

```
$event->property('X-ACME-ROOM-ID')?->value()->toString(); // "4"
// VTIMEZONE / VTODO / VJOURNAL etc. survive as GenericComponent in $calendar->components()
```

Strict vs lenient
-----------------

[](#strict-vs-lenient)

Both the parser and serializer have a strict mode. **Lenient is the default**, because real-world `.ics` files frequently bend the RFC.

ModeParserSerializer**Lenient** (default)Recovers from violations; unparseable values become `RawValue`Emits whatever is present**Strict**Throws `ParseException` on violationsThrows `MissingPropertyException` for missing required properties```
Parser::strict()->parseCalendar($ics);          // validate input
(new IcsSerializer(strict: true))->serialize($c); // validate output before sending
```

Error handling
--------------

[](#error-handling)

Every exception implements `Erenav\ICalendar\Exception\ICalendarException`, so you can catch the whole family at once.

```
use Erenav\ICalendar\Exception\{ICalendarException, ParseException, InvalidValueException, MissingPropertyException};

try {
    $calendar = Parser::strict()->parseCalendar($ics);
} catch (ParseException $e) {
    // malformed input in strict mode
} catch (ICalendarException $e) {
    // any other library error
}
```

- `InvalidValueException` — building an illegal value (bad duration, out-of-range geo, …).
- `ParseException` — malformed input (strict parsing only).
- `MissingPropertyException` — required property absent (strict serialization only).

Gotchas &amp; current limitations
---------------------------------

[](#gotchas--current-limitations)

- **You must set `UID` (and usually `DTSTAMP`) yourself.** They are not auto-generated. `$event->uid()` returns `null` if absent. Use strict serialization to catch this.
- **Use the calendar-level expander for overrides.** `Event::occurrencesBetween()` expands one event in isolation and ignores `RECURRENCE-ID` overrides. To honour modified/cancelled instances, expand the whole calendar with `Calendar::occurrencesBetween()`. Note: `RANGE=THISANDFUTURE` overrides are treated as single-instance for now.
- **Custom (non-IANA) `VTIMEZONE` resolution.** Zoned date-times with standard IANA ids (`America/New_York`) expand DST-correctly, and `withTimeZones()` generates portable `VTIMEZONE` blocks for them. A `TZID` that exists *only* as a `VTIMEZONE` block in the file (not a PHP zone) is preserved and readable as a typed `TimeZone`, but is still treated as UTC for instant math — resolving offsets from custom definitions is deferred.
- **"Level-1" round-trip ≠ byte-identical.** `serialize(parse($ics))` never loses data and preserves property order within a component, but it *canonicalizes* output (line folding position, parameter ordering, escaping). Byte-for-byte fidelity (Level-2) is a future option, not a current guarantee.
- **Immutability surprise:** builder methods that read like mutations (`addAttendee`) mutate the *builder*; the produced component is immutable. Edit an existing component via `->toBuilder()`.
- **No Laravel glue here.** The framework integration (`erenav/laravel-icalendar`) is a separate package (phase 4). This core has zero framework dependencies.

Architecture
------------

[](#architecture)

A layered, immutable object model. The canonical state of every component is its ordered property bag, which is what makes lossless round-tripping possible.

```
Builder      (mutable, fluent)        →  produces  →  Component (immutable)
Component    (Composite: Calendar ▸ Event ▸ Alarm)
  └ holds → PropertyBag (ordered, preserves unknowns)
Property     (name + typed values + parameters)
  ├ value  → ValueType (DateTimeValue, Duration, Period, TextValue, RawValue, …)
  └ params → Parameter (Role, PartStat, … enums + RawParameter fallback)

Parser:     text → unfold → split content lines → hydrate values → assemble tree
Serializer: Component → content lines → fold → text   (Strategy: Ics, future jCal/xCal)

```

Patterns in use: **Composite** (component tree), **Builder** (fluent construction), **Strategy** (`Serializer` interface), **Factory** (value-type construction), and a **pipeline** parser. See [`docs/PHASE-1-SPEC.md`](docs/PHASE-1-SPEC.md) for the full design and decision record.

Testing
-------

[](#testing)

```
composer install
composer test          # or: vendor/bin/phpunit
```

The suite is split into `tests/Unit` (per-class) and `tests/Integration`(serializer + round-trip). Round-trip stability is asserted as a fixed point: `serialize(parse(x))` equals `serialize(parse(serialize(parse(x))))`.

Roadmap
-------

[](#roadmap)

PhaseScopeStatus1Core model, parse/serialize, Level-1 round-trip (RFC 5545 + 7986)✅ done2Recurrence + time zones — `occurrencesBetween()`, `RECURRENCE-ID` overrides, `VTIMEZONE` generation/typed components✅ done3iTIP scheduling (RFC 5546) — METHOD, message builders, validation✅ done4[`erenav/laravel-icalendar`](https://github.com/erenav/laravel-icalendar) — service provider, facade, Eloquent mapping, feeds, Artisan, notifications✅ released separately5jCal/xCal serializers, custom-`VTIMEZONE` offset resolution, byte-fidelity round-tripsomedayLicense
-------

[](#license)

MIT

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/49ad369ef9b2048a4df1684204853a217ce15e661f6aa522bb26ae42e9750b7e?d=identicon)[vanere](/maintainers/vanere)

---

Top Contributors

[![vanere](https://avatars.githubusercontent.com/u/3731011?v=4)](https://github.com/vanere "vanere (7 commits)")

---

Tags

iCalendaricsicalrfc5545rfc5546calendarveventrfc7986

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/erenav-icalendar/health.svg)

```
[![Health](https://phpackages.com/badges/erenav-icalendar/health.svg)](https://phpackages.com/packages/erenav-icalendar)
```

###  Alternatives

[eluceo/ical

The eluceo/iCal package offers an abstraction layer for creating iCalendars. You can easily create iCal files by using PHP objects instead of typing your \*.ics file by hand. The output will follow RFC 5545 as best as possible.

1.2k18.3M57](/packages/eluceo-ical)[kigkonsult/icalcreator

iCalcreator is the PHP implementation of rfc2445/rfc5545 and rfc updates, management of calendar information

2482.8M20](/packages/kigkonsult-icalcreator)[spatie/icalendar-generator

Build calendars in the iCalendar format

6868.2M15](/packages/spatie-icalendar-generator)[sabre/vobject

The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects

59724.6M50](/packages/sabre-vobject)[jsvrcek/ics

abstraction layer for creating multi-byte safe RFC 5545 compliant .ics files

2232.0M11](/packages/jsvrcek-ics)[welp/ical-bundle

Symfony Bundle to manage .ics iCal file (creating and eventually reading)

10115.5k](/packages/welp-ical-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
