spatie/calendar-links
Generate add to calendar links for Google, iCal and other calendar systems
2.1.0
8.1M downloads/mo
#1604 most downloaded on Packagist
spatie/calendar-links
What this package is like to depend on
Last release 3 days ago
20 Aug 2026
Release timing varies
gaps range from 9 days to 13 months
Most releases are documented
notes for 35 of 41 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
41 releases · first in 2017
4 releases in the last 12 months
see the full history below
Release timeline
41 releases · Sep 2017 to Aug 2026Releases
latest 41-
2.1.020 Aug 2026Release notes
Open source →TL;DR: no upgrade steps, the public API is unchanged. Headline additions are guests and separate start/end timezones. ICS files carrying a timezone now name it with a
TZIDand ship aVTIMEZONE. Subclassing a generator? Read the last section.New Features
- Guests (attendees):
Link::guest()/Link::guests(), required and optional roles, rendered asadd(Google),inv_list(Yahoo),to/cc(Outlook),ATTENDEE(ICS) (#229, #237) - Separate start and end timezones, for flights (#233, #246, #249)
- ICS
TRANSP,CLASS,RRULE,X-MICROSOFT-CDO-BUSYSTATUS,DTSTAMPvia the options array (#230) (#244) - ICS extension points:
additionalCalendarProperties(),additionalEventProperties()(#242, #247)
Changed
- ICS files carry a
VTIMEZONEfor everyTZIDthey name, without which older Outlook desktop shifts the event (#249) - A recurring event in a zone that moves its clocks is written as a local time with a
TZID, not a UTC instant, which used to put every occurrence past a DST change an hour off (#254) - Zones naming no place (
+02:00,CEST, POSIX rule sets, all ofEtc/) are written in UTC (#248) - ICS
REMINDERandformatare validated instead of silently falling back, so['format' => 'FILE']no longer returns a link where a file was asked for (#254) - Generators are easier to extend: four escaping helpers are now
protected,Google/YahooreadBASE_URLlate statically (#242) - A
falseURL parameter renders as&key, not&key=
Bug Fixes
- ICS values could inject calendar properties through
UID,PRODID,URL,RRULE, the enumerated options, andATTENDEEaddresses assigned straight toLink::$guests(#245, #254) - RFC 5545 gaps: long lines are folded without splitting a multibyte character, the file ends with CRLF, the data URI declares
charset=utf-8, invalid control characters are dropped (#250) - ICS event properties landed after
VALARM(#247) VTIMEZONEstopped at the year it described. The last change of each kind now carries a yearly rule where the zone repeats predictably (#254)- All-day events across timezones gained or lost a day (#237)
- Yahoo deep link parameters: no more stale
view=d&type=20, wall clockST/ET,DUR=alldayfor single days, exclusiveETfor multi-day (#232) - Attendee escaping: a
CAL-ADDRESSis a URI, not TEXT. Unusable guest addresses are rejected (#229) - A description or address of
'0'is no longer dropped as empty
Notes for subclasses
The public API is unchanged, but three changes reach a subclass. The first two are fatal at load time:
Ics::$presentationOptionsis now typedarray. Redeclaring it untyped raisesType of Sub::$presentationOptions must be array. Drop the redeclaration or type it to match.Ics::generateAlertComponent(),Yahoo::sanitizeText(),Yahoo::sanitizeAddressList()andBaseOutlook::sanitizeString()wentprivatetoprotected. A same-namedprivatemethod raisesAccess level to Sub::sanitizeText() must be protected or weaker. Widen it.Yahoo::BASE_URLdroppedview=d&type=20, so a subclass building its own URL from the constant gets the shorter value.
Made by @alies-dev and the community.
Full Changelog: 2.0.1...2.1.0
Release notes
Open source →Added
- Add first-class guests (attendees) via
Link::guest()andLink::guests(), with required and optional roles. Guests are rendered asaddfor Google,inv_listfor Yahoo,toandccfor Outlook, andATTENDEEproperties in ICS (#229) - Support separate start and end timezones (useful for flights): new
Link::$fromTimezone,Link::$toTimezoneandLink::hasDistinctTimezones(). Google links usestzandetz, ICS files write each endpoint with its ownTZIDalongside a matchingVTIMEZONEcomponent, as RFC 5545 requires. Two spellings of one zone (UTCandEtc/UTC, or the bareZthat PHP leaves on aZsuffixed ISO string) count as a single zone, so only an event that genuinely crosses zones takes this path (#233, #246, #249) - ICS: support
TRANSP,CLASS,RRULEandX-MICROSOFT-CDO-BUSYSTATUSvia the options array (#230) - ICS: support a custom
DTSTAMPvia the options array. It defaults to the event start, which keeps the sameLinkproducing the same bytes, so pass your own value when you need it read as a revision signal (#244) - Support repeated URL parameters in URL generators: an array value in
$urlParametersis rendered as a repeated query parameter (#228) - ICS: subclasses can append extra properties through the new
additionalCalendarProperties()andadditionalEventProperties()extension points, emitted before the nestedVALARMcomponent so the output stays valid (#242, #247)
Changed
- Generators are easier to extend:
Ics::generateAlertComponent(),Yahoo::sanitizeText(),Yahoo::sanitizeAddressList()andBaseOutlook::sanitizeString()are nowprotected, andGoogle/YahooreadBASE_URLlate statically so a subclass can redefine it - ICS: a recurring event in a timezone that moves its clocks is now written as a local time named by a
TZID, with a matchingVTIMEZONE, where it used to be written as a UTC instant. AnRRULErepeats the local time of itsDTSTART, so a start pinned to UTC put every occurrence past a daylight saving change an hour away from the time the event was booked for. A recurring event in UTC, in a zone that keeps one offset the year round, or in a zone that names no place is unaffected, and so is any event without anRRULE - ICS:
REMINDERnow has to be an array, itsTIMEaDateTimeInterfaceand itsDESCRIPTIONa string. A value of another type used to be dropped for the default alarm without a word, which handed back a reminder nobody asked for - ICS: the presentation
formatnow has to behtmlorfile. Anything else used to fall through to the data URI, so['format' => 'FILE']silently returned a link rather than the file it asked for - URL generators render a
falseparameter value as a bare flag (&key) rather than as an empty assignment (&key=)
Fixed
Link::guest()andLink::guests()now ignore an address that is already on the guest list, compared without regard to case, so nobody is invited twice- ICS: an
ATTENDEEaddress can no longer inject calendar properties.guest()rejects a control character, butLink::$guestsis a public property that can be assigned around it, and a CR or an LF in an address ended theATTENDEEproperty and started another. Control characters are now percent encoded into themailtoURI - ICS:
URLandRRULEnow reject every control character rather than only CR and LF. Neither a URI nor a recurrence rule admits one, and writing it out produced a file no parser accepts - ICS: the
VTIMEZONEcomponents now carry a yearly recurrence rule on the last change of each kind, whenever the zone repeats that change on a fixed weekday of a fixed month. Without one the component stopped at the year it described, and a recurring event past that resolved against the wrong offset for part of every later year on a client that does not resolve bare IANA names - A description or address of
'0'is no longer dropped from the output as if it were empty - The exception for a negative date range no longer claims TO must be greater than FROM, since equal instants are accepted, and it now prints each time with its timezone
- All-day events whose start and end dates carry different timezones no longer gain or lose a day. The end date is now read as the calendar date it was written as, instead of being converted as an instant
- Timezones that name no place (an offset such as
+02:00, an abbreviation such asCEST, the POSIX rule sets, and the wholeEtc/tree) are written in UTC instead of being named. Google silently ignores actzit cannot resolve, which left the event at the wrong instant for every viewer outside that offset, and an offset in an ICSTZIDcut the property value in half at its colon. Parsing an ISO 8601 string with an offset is the everyday way to reach this (#248) - ICS: values passed through the options array can no longer inject calendar properties.
UIDandPRODIDare escaped as the TEXT values they are,URLandRRULEreject a carriage return or line feed, the enumerated properties are checked against their allowed tokens, and a customREMINDER.DESCRIPTIONis escaped like the default one (#245) - ICS: close several RFC 5545 gaps. Content lines longer than 75 octets are folded (without splitting a multibyte character), the file ends with a CRLF, the data URI declares
charset=utf-8rather than the unregisteredutf8, and control characters invalid in a TEXT value are dropped (#250) - Yahoo: align deep link parameters with the service parser. The base URL drops the stale
view=d&type=20pair, timed events send wall clockSTandETvalues instead of UTC, single day all-day events sendDUR=allday, and multi day all-day events send an exclusiveETend date (#232) - ICS: correct attendee escaping (a
CAL-ADDRESSis a URI, not TEXT) and reject guest email addresses that no calendar service can carry (#229)
Docs
- Rework the README examples and fix the stale Google output (#231)
- Document that Yahoo composes a multi day all-day event as a timed one running from midnight to midnight, since
DUR=alldayand anETend date cannot be sent together (#243)
Notes for subclasses
The public API is unchanged, but two of the changes above are visible to a class extending a generator, and either can stop it loading:
Ics::$presentationOptionsis now declaredarray, where 2.0.x left it untyped. A subclass that redeclares the property without a type raisesType of Sub::$presentationOptions must be array. Drop the redeclaration, or type itarrayto matchIcs::generateAlertComponent(),Yahoo::sanitizeText(),Yahoo::sanitizeAddressList()andBaseOutlook::sanitizeString()went fromprivatetoprotected. A subclass that happens to declare aprivatemethod of the same name raisesAccess level to Sub::sanitizeText() must be protected or weaker. Widen it toprotected, which is what it now overridesYahoo::BASE_URLno longer carries the staleview=d&type=20pair. A subclass that built its own URL from the constant gets the shorter value
- Guests (attendees):
-
2.0.129 May 2026 -
1.11.116 Jan 2025Release notes
Open source →What's changed:
- Fix ics alert not working when a date is set #202 by @giacomovernoni
Full Changelog: 1.11.0...1.11.1
-
1.11.019 Feb 2024Release notes
Open source →What's Changed
- Allow URL customisations for Google, Outlook and Yahoo by @alies-dev in #191
- Fix attempt for Google and "All day". by @vever001 in #192
New Contributors
Full Changelog: 1.10.0...1.11.0
-
1.10.026 Jan 2024Release notes
Open source →What's Changed
- Add reminder functionality to ICS by @S-Braeutigam and @alies-dev in #178 1ffc15a
New Contributors
- @S-Braeutigam made their first contribution in #178
Full Changelog: 1.9.0...1.10.0
-
1.9.026 Jan 2024Release notes
Open source →What's Changed
- ICS: support different presentation (
file,html) by @alies-dev in #185 - Update dependencies; bump min PHP version to 8.1 by @alies-dev in #187
Full Changelog: 1.8.6...1.9.0
- ICS: support different presentation (
-
1.8.626 Jan 2024 -
1.8.502 Mar 2023Nothing published for this version
-
1.8.401 Mar 2023Nothing published for this version
-
1.8.301 Mar 2023Nothing published for this version
-
1.8.211 Dec 2022Release notes
Open source →Changed
- ICS: Use
DESCRIPTIONinstead ofX-ALT-DESC(as it has better support) by @cdubz in #158 - Chore: fix tests, fix and improve CI
- ICS: Use
-
1.8.101 Dec 2022 -
1.8.020 Aug 2022Release notes
Open source →Changed
- ICS: Add
PRODIDandDTSTAMPrequired parameters to make ICS valid by @makbeta - ICS: Fix HTML description for Outlook 2016 by @karthikbodu
- Outlook: extract common logic for
WebOfficeandWebOutlookinto a parent class by @lptn
Fixed
- Simplify format of test snapshots: do not use base64 by @lptn
- Fix typo in README by @fabpot
- ICS: Add
-
1.7.209 Jun 2022Release notes
Open source →Fixed
- Outlook: Fixed #148 Support HTML-formatted description by @dravenk in #150
-
1.7.113 Feb 2022Release notes
Open source →Changed
- Outlook: Fixed location field characters (by @dravenk in #144)
- Add missing dependency of php-cs-fixer and update it
-
1.7.013 Feb 2022Release notes
Open source →Changed
- New: Add support for outlook.office.com $link->webOffice(); (@dravenk and @gulios)
- Google: Add timezone name if it is specified in both
fromandtodates and is the same for both (@bradyemerson)
-
1.6.022 Apr 2021 -
1.5.022 Apr 2021Release notes
Open source →Changed
- ICS: support URLs as option (@gulios)
- ICS: support all day events spanning multiple days (@mrshowerman)
-
1.4.413 Apr 2021Release notes
Open source →Fixed
- Yahoo link doesn’t work (yahoo changed param names) (@mukeshsah08).
- Exception message on invalid dates range (idea by @jason-nabooki)
-
1.4.305 Mar 2021Release notes
Open source →Changed
- Google: use UTC timezone to bypass problems with some timezone names unsupported by Google calendar (⚠️ backwards-incompatible if you extended Google Generator)
Fixed
- Spaces replaced by "+" on Outlook.com #109
-
1.4.201 Sep 2020 -
1.4.127 Aug 2020Release notes
Open source →Changed
- Simplify extending of WebOutlook (e.g. for Office365)
- Yahoo: use
alldayparameter only for a single-day events - Improve exception hierarchy:
InvalidLinknow extends\InvalidArgumentException
Added
- Add more tests, reorganize existing
-
1.4.002 May 2020Release notes
Open source →Added
- Allow specifying custom
UIDICS links (https://github.com/spatie/calendar-links/pull/85) - Support PHP 8.0
- Support immutable dates (
\DateTimeImmutable::class)
Changed
- Require PHP 7.2+
- Allow specifying custom
-
1.3.029 Apr 2020Release notes
Open source →- Support custom generators (
$link->formatWith(new Your\Generator()) - Fix iCal links that contains special chars (use base64 for encoding)
- Fix Outlook links: use new base URI and datetime formats
- Fix Yahoo links: events had invalid end datetime (due to a bug on Yahoo side)
- Support custom generators (
-
1.2.417 Jul 2019Release notes
Open source →- Fix Google links for all-day events (use next day as end-date for single-day events)
- Fix Outlook links for all-day events (omit
enddtfor single-day events) - Add a new
Link::createAllDaystatic constructor to simplify creating of all-day events
-
1.2.314 Feb 2019 -
1.2.215 Jan 2019 -
1.2.113 Jan 2019Release notes
Open source →- Fix iCal: Use CRLF instead of LF (according RFC 5545)
- Fix iCal: Specify UID property (according RFC 5545)
- Fix iCal: Escape
;character (according RFC 5545) - Fix iCal: Remove empty new line from .ics files
-
1.2.010 Jan 2019 -
1.1.108 Oct 2018 -
1.1.013 Aug 2018 -
1.0.323 Jul 2018 -
1.0.215 May 2018 -
1.0.130 Apr 2018 -
1.0.028 Sep 2017 -
0.1.128 Sep 2017Nothing published for this version
-
0.1.028 Sep 2017Nothing published for this version
-
0.0.117 Sep 2017Nothing published for this version
-
2.0-beta.110 Feb 2026Release notes
Open source →What's Changed
- Fix all-day events via constructor being 1 day short by @freekmurze in #211
- Fix ics alert not working when a date is set by @giacomovernoni in #202
Internal Updates
- Bump stefanzweifel/git-auto-commit-action from 5 to 6 by @dependabot[bot] in #205
- Bump actions/checkout from 4 to 5 by @dependabot[bot] in #206
- Bump stefanzweifel/git-auto-commit-action from 6 to 7 by @dependabot[bot] in #208
- Bump actions/checkout from 5 to 6 by @dependabot[bot] in #209
- Update issue template by @AlexVanderbist in #207
New Contributors
- @giacomovernoni made their first contribution in #202
- @AlexVanderbist made their first contribution in #207
Full Changelog: 2.0-alpha.1...2.0-beta.1
-
2.0-alpha.119 Feb 2024Release notes
Open source →What's Changed
- ICS: allow PRODID customization by @alies-dev
- Use native types, finalize classes by @alies-dev in #171
- Bump min PHP version to 8.1 by @alies-dev in #171
- Privatize more methods and properties by @alies-dev in #189
BC breaks
API changes:
Link::create(...)doesn't have a 4th parameter to declare event as a full-day. Instead, useLink::createAllDay(...)
If you extended classes from the package or used non-private members directly, these BC changes may be breaking for you:
- REMOVED: Method
Spatie\CalendarLinks\Link#__get()was removed - CHANGED: Type of property
Spatie\CalendarLinks\Link#$titlechanged from having no type to string - CHANGED: Type of property
Spatie\CalendarLinks\Link#$fromchanged from having no type toDateTimeImmutable - CHANGED: Type of property
Spatie\CalendarLinks\Link#$tochanged from having no type toDateTimeImmutable - CHANGED: Type of property
Spatie\CalendarLinks\Link#$descriptionchanged from having no type to string - CHANGED: Property
Spatie\CalendarLinks\Link#$descriptionchanged default value from NULL to '' - CHANGED: Type of property
Spatie\CalendarLinks\Link#$allDaychanged from having no type to bool - CHANGED: Type of property
Spatie\CalendarLinks\Link#$addresschanged from having no type to string - CHANGED: Property
Spatie\CalendarLinks\Link#$addresschanged default value from NULL to '' - CHANGED: Method
__construct()of classSpatie\CalendarLinks\Linkbecame final - CHANGED: The return type of
Spatie\CalendarLinks\Link::create()changed from no type to static - CHANGED: The return type of
Spatie\CalendarLinks\Link::createAllDay()changed from self to the non-covariant static - CHANGED: The return type of
Spatie\CalendarLinks\Link::createAllDay()changed from self to static - CHANGED: Parameter 1 of
Spatie\CalendarLinks\Link::createAllDay()changed name from fromDate to from - CHANGED: The return type of
Spatie\CalendarLinks\Link#description()changed from no type to static - CHANGED: The return type of
Spatie\CalendarLinks\Link#address()changed from no type to static - REMOVED: Method
Spatie\CalendarLinks\Exceptions\InvalidLink::invalidDateRange()was removed - REMOVED: Property
Spatie\CalendarLinks\Generators\Yahoo#$dateFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\Yahoo#$dateTimeFormatwas removed - CHANGED: Class
Spatie\CalendarLinks\Generators\WebOfficebecame final - REMOVED: Constant
Spatie\CalendarLinks\Generators\WebOffice::BASE_URLwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateTimeFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$urlParameterswas removed - CHANGED: Constant
Spatie\CalendarLinks\Generators\WebOffice::BASE_URLvisibility reduced from protected to private - CHANGED: Method baseUrl() of class
Spatie\CalendarLinks\Generators\WebOfficevisibility reduced from public to protected - CHANGED: Type of property
Spatie\CalendarLinks\Generators\Ics#$dateFormatchanged from having no type to string - CHANGED: Type of property
Spatie\CalendarLinks\Generators\Ics#$dateTimeFormatchanged from having no type to string - CHANGED: Type of property
Spatie\CalendarLinks\Generators\Ics#$optionschanged from having no type to array - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateTimeFormatwas removed - CHANGED: Method baseUrl() of class
Spatie\CalendarLinks\Generators\BaseOutlookvisibility reduced from public to protected - CHANGED: Class
Spatie\CalendarLinks\Generators\WebOutlookbecame final - REMOVED: Constant
Spatie\CalendarLinks\Generators\WebOutlook::BASE_URLwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$dateTimeFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\BaseOutlook#$urlParameterswas removed - CHANGED: Constant
Spatie\CalendarLinks\Generators\WebOutlook::BASE_URLvisibility reduced from protected to private - CHANGED: Method baseUrl() of class
Spatie\CalendarLinks\Generators\WebOutlookvisibility reduced from public to protected - REMOVED: Property
Spatie\CalendarLinks\Generators\Google#$dateFormatwas removed - REMOVED: Property
Spatie\CalendarLinks\Generators\Google#$dateTimeFormatwas removed
Full Changelog: 1.11.0...2.0-alpha.1
-
2.010 Feb 2026Release notes
Open source →Release Notes: v2.0
TL;DR: for 95% installations, no actions required. The biggest change:
Link::create()is not longer has 3rd parameterallDay: you need to useLink::createAllDay()instead.Breaking Changes
- PHP 8.3+ required — minimum PHP version bumped from 8.1 to 8.3
$fromand$toare nowDateTimeImmutable— previouslyDateTime- Link properties are now public readonly —
$title,$from,$to, and$allDayare public readonly;$descriptionand$addressare public. The magic __get() method has been removed. Link::create()no longer accepts $allDay parameter — use Link::createAllDay() for all-day events insteadLink::createAllDay()end date semantics changed — the $numberOfDays parameter now represents inclusive days (e.g.,createAllDay('Event', $date, 1)creates a single-day event). The constructor now internally adds +1
day to convert to the exclusive end date expected by calendar services.- Link constructor is now final — subclassing the constructor is no longer possible; use static factory methods
InvalidLink::invalidDateRange()removed — the deprecated method has been deleted; use InvalidLink::negativeDateRange() insteadBaseOutlook::baseUrl()visibility changed — from public to protectedWebOfficeandWebOutlookclasses are now final- Generator constants are now typed — e.g., const string FORMAT_HTML = 'html' (PHP 8.3 typed constants)
- Instance properties replaced with class constants —
$dateFormat,$dateTimeFormatin Google, Yahoo, and BaseOutlook are now private const values, preventing override via subclassing
New Features
- Custom ICS
PRODID— the Ics generator now accepts a PRODID option to customize the product identifier in generated ICS content Link::createAllDay()returns static — previously returned self, now supports late static binding for subclasses
Bug Fixes
- All-day events off by one day — fixed all-day events created via the constructor being 1 day short
- ICS DTSTAMP RFC 5545 compliance — DTSTAMP now always uses UTC datetime for all-day events (previously used date-only format)
- ICS DTSTART for all-day events — added VALUE=DATE parameter to DTSTART for all-day events per RFC 5545
- ICS escape string RFC 5545 compliance — escapeString() now properly handles backslashes, semicolons, commas, and newlines per RFC 5545 section 3.3.11 (previously used addcslashes which was non-compliant)
- ICS VALARM trigger fix — fixed reminder/alert not working when a specific date was set as the trigger time
Internal / Code Quality
declare(strict_types=1)added to all source files- Modern PHP 8.3+ features: typed class constants,
#[\Override]attributes DateTimeImmutableused throughout instead of mutableDateTime- PHPUnit
#[Test]attributes replace@testannotations - PSR-12 coding style applied
- Psalm upgraded to v6.15 with improved type annotations (@api, non-empty-string, non-empty-list)
- Removed unnecessary clone calls thanks to immutable date objects
Made by @alies-dev and the community.
Full Changelog: 1.11.1...2.0