# icalendar-generator: building RFC 5545 calendars from a fluent PHP API

> Spatie's icalendar-generator produces iCalendar feeds for phones and Google Calendar from a fluent PHP builder, implementing RFC 5545 and some of RFC 7986 without making you read either. It is a small package with a narrow job, and its sharpest edge is the inclusive and exclusive end date rule that the spec imposes.

**spatie/icalendar-generator** — Generate calendars in the iCalendar format

- Repository: https://github.com/spatie/icalendar-generator
- Website: https://spatie.be/open-source
- Stars: 696 · Forks: 66
- Language: PHP
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-icalendar-generator

## A narrow job, done properly

The problem is a small one that a surprising number of applications need solved. You want to publish a calendar that people can subscribe to, so it shows up in an iPhone's calendar app or in Google Calendar, and you do not want to hand-write the iCalendar format.

The format is defined in RFC 5545, and the README is blunt about the reading experience: the specification is not a pleasant reading experience. The package implements RFC 5545 and some extensions from RFC 7986 to provide an easy to use API for creating calendars, and it is not the intention to implement these RFCs entirely, but to provide a straightforward API that is easy to use.

That framing is the whole design. This is not a general purpose iCalendar library with a partial implementation, it is a small opinionated layer that covers the properties an application actually sets and leaves the rest of the spec alone.

The result is a fluent builder with two entry points, Calendar and Event, and a get() method that returns the serialised text. The library lives in the Spatie namespace, is distributed on Packagist as spatie/icalendar-generator, and the licence is MIT.

The repository is small and conventional: src/ and tests/ at the top, a phpunit.xml for the test suite, phpstan.neon with a baseline file for static analysis, .php_cs.dist.php for style, a CHANGELOG.md, and an UPGRADING.md for the major version migration.

## The builder and the string it produces

The introductory example is short enough to read as documentation:

```php
use Spatie\IcalendarGenerator\Components\Calendar;
use Spatie\IcalendarGenerator\Components\Event;

Calendar::create('Laracon online')
    ->event(Event::create('Creating calender feeds')
        ->startsAt(new DateTime('6 March 2019 15:00'))
        ->endsAt(new DateTime('6 March 2019 16:00'))
    )
    ->get();
```

Calling get() returns a string in the iCalendar format, and the README shows what that string looks like. It opens with BEGIN:VCALENDAR, carries VERSION:2.0 and PRODID:spatie/icalendar-generator, sets NAME and X-WR-CALNAME, then a BEGIN:VEVENT block with a UID, a DTSTAMP, a SUMMARY, a DTSTART and a DTEND, and closes with END:VEVENT and END:VCALENDAR.

Two details in that output are worth pausing on. The PRODID identifies the producing software, which is why the library sets it to its own package name. And the event carries two DTSTAMP properties, one with a TZID parameter and one without, which is a quirk of the example output rather than something the API asks you to produce.

The X-WR-CALNAME property is the non-standard extension that makes the calendar name appear in some clients, and it comes from the name you give the calendar. The name argument to create() is the short form, and there is also a name() method for setting it later, alongside description() for a calendar description.

The end of the chain is get(), and the README is explicit about why: in the end you want to convert your calendar to text so that it can be streamed or downloaded to the user.

## The non-inclusive end date is the trap

There is one rule in the specification that catches everybody, and the README deals with it directly.

The iCalendar spec defines the end of an event as non inclusive, so the end date is the day after the last day of the event. The README gives the example: an event that starts on 6 March 2019 and ends on 7 March 2019 takes place on March 6th only, and your calendar application will display it on March 6th.

That is the opposite of what most programming languages do with an end date. In a typical function signature, an end date of 7 March with a start of 6 March means the event covers both days. Here it means the event covers one day and the end is an exclusive bound.

The fullDay() method exists for the all-day case, and the example pairs it with explicit start and end dates:

``` php
Event::create()
    ->fullDay()
    ->startsAt(new DateTime('6 March 2019'))
    ->endsAt(new DateTime('7 March 2019'))
```

So the same exclusive rule applies to a full day event, and the developer has to add a day to the end date to cover the day they mean. Any code that computes an end date from a start date plus a duration will be off by one day unless it accounts for this.

This is a good example of a library doing its job well and still requiring the caller to understand the spec. The alternative would have been to hide the rule and make the API intuitive, and then the generated file would be wrong for any client that follows the specification.

## Attendees, organisers and the participation statuses

The event API covers the properties a real calendar feed needs beyond the bare minimum, and the design keeps required and optional fields in the right places.

For attendees, only an email address is required, and a name is optional. You can set the participation status as a third argument, and there are five statuses: Accepted, Declined, Tentative, NeedsAction and Delegated. An attendee can also be marked as required to RSVP, which is a separate flag on the call:

``` php
Event::create()
    ->attendee('ruben@spatie.be', 'Ruben', ParticipationStatus::NeedsAction, requiresResponse: true)
    ...
```

For organisers, the email address is required and the name can be omitted. There is an encoding detail here that shows the care in the package: line breaks in organiser and attendee names are encoded as ^n, including CRLF, CR and LF input. That is a spec requirement, and a naive implementation that only handled one line ending would produce a malformed feed that some clients reject and others silently mis-render.

Events also carry a location, with an address, an address name and coordinates, and there is a transparent() method that marks an event as not visually overlapping others in a calendar. A uniqueIdentifier can be set, which matters for updates, because a client uses the UID to decide whether an event is new or an edit of something it already has. createdAt sets the DTSTAMP, and period() takes a start and an end in one call when you have both.

## Streaming a calendar and the refresh interval

The feature that matters for a live site is the refresh interval. The README explains that when streaming a calendar to an application, it is possible to set the calendar's refresh interval by duration in minutes, and that when this is set, the calendar application will check your server every time after the specified duration for changes to the calendar:

``` php
Calendar::create('Laracon Online')
    ->refreshInterval(5)
    ...
```

This is the property that turns a static file into a subscription. Without it, a client that supports live calendars will poll on whatever schedule it decides, or a client that does not support them will fetch once and never update. With a five minute interval, the client is told to come back every five minutes.

That is a server-side feature even though the output is a text file, and it is the reason the README talks about streaming rather than only downloading. A generated calendar is normally served from a route that calls the builder and returns the text, so the same endpoint serves both the initial subscription and the periodic refresh.

The practical consequence is that the endpoint behind the calendar becomes part of your application's live surface. It has to be fast, it has to be available when clients poll, and it has to produce a consistent UID for the same event across requests, otherwise clients will treat every refresh as a new set of events. The uniqueIdentifier on the event is what makes that possible.

## Where this package is the wrong tool

The first limitation is the one the project states for itself. It is not their intention to implement these RFCs entirely, but to provide a straightforward API. If you need the whole specification, or a specific extension from RFC 7986 that is not covered, this is not the library, and you will end up writing raw property lines anyway.

The second limitation is the PHP version. The package requires PHP 8.4 or higher, which is a real constraint for a library that many people will reach for in an existing application on an older runtime.

The third limitation is the shape of the output. What you get is a string, and it is a string that you write to a response body. The package does not decide the HTTP headers, the caching behaviour, the content type or the URL. That is not a criticism, since most frameworks have their own opinion about responses, but it means the last mile of a working subscription is still your code.

The real alternative is a full calendar service or a framework package that covers the whole format. The difference in approach is scope. This package gives you a small, readable, MIT licensed builder that covers the properties an application normally sets, and it lets you drop down to raw text when you need something it does not model. A complete implementation gives you the entire specification and, with it, a much larger surface, more to read, and often a heavier dependency footprint. The other alternative, doing it by hand, means owning line folding, escaping and the UID discipline, which is exactly the work this package exists to remove. For a Laravel application that needs to publish its event list as a calendar, this is the reasonable choice; for a project that needs every corner of the specification, it is not.

## Conclusion

Adopt icalendar-generator if you are in PHP and need to publish a subscribeable calendar, because the fluent API removes the line folding, escaping and property formatting that a hand-written iCalendar writer gets wrong. Do not adopt it if you need the full RFC 5545 surface, since the project states outright that the intention is a straightforward API rather than complete implementation, and do not adopt it if your platform is not on PHP 8.4 or higher, which is the stated requirement. Before you publish a feed, read the documentation on the non-inclusive end date, because an event that looks two days long will be rendered as one day by most clients, and test the output by subscribing to it from a real phone rather than trusting the string.

## FAQ

### What does spatie/icalendar-generator do?

It generates calendars in the iCalendar format, RFC 5545, so they can be displayed in an iPhone's calendar app or in Google Calendar. It implements RFC 5545 and some extensions from RFC 7986 and is MIT licensed.

### How do I install icalendar-generator?

The package requires PHP 8.4 or higher and installs with composer require spatie/icalendar-generator. It is published on Packagist as spatie/icalendar-generator, and there were substantial changes between v1 and v2 with an upgrade guide for that migration.

### How do I create a calendar with icalendar-generator?

Use Calendar::create(), optionally with a name, add events with the event() method, and call get() to return the serialised text. The chain can also set a description, a refreshInterval in minutes so subscribed clients re-check the feed, and each Event can carry a name, description, start, end, location, organiser and attendees.

### Why does my event show up as one day instead of two?

The iCalendar spec defines the end of an event as non inclusive, so the end date is the day after the last day of the event. An event starting 6 March 2019 and ending 7 March 2019 takes place on March 6th only, and your calendar application will display it on March 6th.

### How do I set an attendee's participation status in icalendar-generator?

The status is the third argument of attendee(), and there are five values: ParticipationStatus::Accepted, Declined, Tentative, NeedsAction and Delegated. A fourth named argument, requiresResponse, marks the attendee as required to RSVP, and only an email address is required for an attendee.

## Sources

- [License: MIT](https://github.com/spatie/icalendar-generator/blob/main/LICENSE)
- [Project website](https://spatie.be/open-source)
- [README](https://github.com/spatie/icalendar-generator/blob/main/README.md)
- [Releases](https://github.com/spatie/icalendar-generator/releases)
- [spatie/icalendar-generator on GitHub](https://github.com/spatie/icalendar-generator)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-icalendar-generator
