Changelog
5.0.0 (unreleased)
Section titled “5.0.0 (unreleased)”Version 5 adds a new, layered API (om\ICal) and keeps the array based API of version 4 (IcalParser, EventsList,
Freq, Recurrence, ParserOptions) with the shape of its data, now deprecated and to be removed in 5.5 at the latest. Both use a new recurrence engine and
content line parser, which fixes many bugs; the results of affected calendars differ from 4.x.
See UPGRADING.md. The fixes of 4.1.4 (#37, #88) are included.
New API
Section titled “New API”ICal::parse(),ICal::parseFile(),ICal::stream()and the configurableICal::parser()withParserMode::Strict/Permissive,ParseLimits,RecurrenceLimitsand aParseResultwith structured warnings- syntax layer:
ContentLine,Parameters,LineReader(streams in chunks),Tokenizer - immutable generic model
Component/Propertykeeping unknown and X- properties; typed facadesCalendar,Event,Todo,Journal,FreeBusy,Alarm,TimezoneDefinition DateTimeValuekeeps DATE, floating, UTC and zoned times apart; floating times need an explicit timezoneValueParserfor all RFC 5545 value types- timezone resolvers: VTIMEZONE definitions (custom Outlook/Exchange zones are matched to IANA timezones), IANA names, aliases (CLDR Windows names, Outlook display names, prefixed and shortened names, intl), fallback
- series: overrides grouped by UID, moved, cancelled and
RANGE=THISANDFUTUREinstances, lazyoccurrencesBetween()andoccurrences(limit) Validatorwith severities,Serializerwith UTF-8 safe folding- vCalendar 1.0
ENCODING=QUOTED-PRINTABLEtext values are decoded (with a warning), as byIcalParser - exceptions with error code, line, property and raw value (
SyntaxException,InvalidValueException,InvalidRecurrenceRuleException,TimezoneResolutionException,ResourceLimitException,ValidationException) - documentation in
docs/, examples inexamples/ - default value types of RFC 7986, RFC 9074 and RFC 9253 properties:
IMAGE,CONFERENCE,SOURCE,LINKandCONCEPTare URIs,REFRESH-INTERVALis aDURATION,ACKNOWLEDGEDis aDATE-TIME(theValidatorreports a non-UTC value);VALUE=UIDandVALUE=XML-REFERENCEof RFC 9253 (#91) - typed getters of RFC 7986:
color()andimages()(Image) of calendars and items,Calendar::source(),Calendar::refreshInterval()andItem::conferences()(Conference); of RFC 9253:Item::links()(Link) andItem::relatedTo()(RelationwithRELTYPEandGAP); of RFC 9073:Item::locations()(Locationof VLOCATION components, kept inside their event or task also when their END is missing); of RFC 9074:Alarm::uid()andAlarm::acknowledged()(#92) - creating calendars with named arguments:
Calendar::create()withname,description,color,method,events,todos,journals,componentsandproperties(backward compatible),Event::new(),Todo::new(),Journal::new(),Location::new(),Alarm::display(),audio()andemail(),CalAddress::create(); values are formatted and escaped, invalid combinations are rejected, a VTIMEZONE is generated for every TZID used (VTimezoneBuilder),Calendar::writeFile();Status,ClassificationandTransparencyenums (#108)
- RFC 5545 recurrence engine
om\RRule\Ruleandom\RRule\Expanderwith all rule parts:BYSETPOS,BYSECOND,SECONDLY, negativeBYWEEKNOandBYYEARDAY,WKSTfor weekly intervals, date-onlyUNTIL - RFC 7529:
RSCALEandSKIP=OMIT|BACKWARD|FORWARDforRSCALE=GREGORIAN; other calendar systems and leap months are not expanded as Gregorian (recurrence.unsupported-rscale) (#90) IcalParser::__construct()acceptsParserOptions;untilIntervalandshiftEventDatesnow work and new options arenow(reproducible results),maxOccurrencesandstrictDTENDof events and instances is derived fromDURATION; all-day events withoutDTENDlast one dayIcalParser::parseDuration()forDURATIONvaluesgetTodos()andgetJournals();DUEandCOMPLETEDare parsed as datesRDATE;VALUE=PERIODvalues (represented by their start)EXDATE;VALUE=DATEremoves the occurrence of that day- quoted parameter values containing
:,;or,(e.g.CN="Doe, John",ALTREP="http://…") - case-insensitive property, parameter and component names; CR line endings; UTF-8 byte order mark; folding with a tab
om\TimezoneResolver(TZID resolution with a cache)
Changed
Section titled “Changed”- the parsed data no longer contains
BEGIN => VCALENDARand0 => nullentries created by blank or invalid lines _RECURRENCE_IDSis grouped by UID:[uid][recurrence-id] => event- parameter values are unquoted (
ORGANIZER-CN, attendee parameters) - properties following a nested component (e.g. after
END:VALARM) belong to the parent component; properties of unknown andX-components are stored under the component name CATEGORIESitems are trimmed and escaped commas no longer split them- an invalid
RRULEis ignored (the event keepsDTSTARTandRDATE); withstrict: trueit throwsInvalidArgumentException - events with an
EXDATEbut noRRULEorRDATEgetRECURRENCESas well; an empty recurrence set produces no event DTSTARTandDTENDof recurring instances are copies, changing them does not modify the parsed data- the callback of
parseString()receives property rows only (notBEGIN:VCALENDAR) and counter0for calendar properties parseFile()throwsRuntimeExceptionwhen the file cannot be read; invalid input keeps previously parsed dataIcalParser::$timezoneis reset for every calendar that is not appendedFreqis an adapter over the new engine: invalid rules throwInvalidArgumentException,maxOccurrenceslimits the expansion,Freq::$debughas no effect,lastOccurrence()returnsfalsefor an empty set andpreviousOccurrence()returnsfalsewhen there is no earlier occurrence (4.x returned DTSTART)
- VTIMEZONE rules with
UNTILare compared in UTC: definitions east of UTC whose daylight saving time ended (e.g. Europe/Moscow before 2011) are resolved again, west of UTC no transition afterUNTILis kept (#110) RDATEwithoutRRULEno longer fails withTypeErrorand adds no yearly occurrences (#37, also in 4.1.4)RDATEvalues are always part of the recurrence set (one was lost together withCOUNT)- a
RECURRENCE-IDreplaces only the matching instance of the same UID, compared as an instant in any timezone (4.x compared strings for all events, so it could hide instances of other events or a wrong instance) - an excluded or overridden first occurrence is no longer returned with the original
DTSTART - series with
COUNTreaching beyond the 3 year horizon are complete (4.x failed withTypeError) - yearly rules in January return every year, not only the first occurrence (#59)
- rules the previous engine expanded incorrectly, for example negative weekday ordinals (
BYDAY=-2MOreturned every third Monday),BYHOURcombined withBYMINUTE(minutes were lost) andINTERVALof weekly rules (worked around in the parser only partially);Freqwith a string rule no longer loops forever - the process default timezone is never changed during expansion
- ambiguous local times (DST fall-back) are their first occurrence and nonexistent times (DST gap) use the offset before the gap, as RFC 5545 requires; PHP alone is not consistent
- sub-daily rules no longer repeat an instant over a DST gap and skip days and hours that cannot match
- impossible rules end after an empty 400-year Gregorian cycle; BYSETPOS selecting nothing no longer loops
- the Windows timezone map is generated from CLDR (
UTCisEtc/UTC,Pacific Standard Time (Mexico)isAmerica/Tijuana, 40 new names); Outlook display names are kept in a separate file
Performance
Section titled “Performance”Compared with 4.1.3 on the sample calendars: expanding recurring events is about 4x faster, parsing a 27 MB calendar with 50 000 events is about 20 % faster with lower peak memory, and sorting 50 000 events is about 13x faster. The new API parses the same 27 MB calendar in 1.1 s with value checks (0.95 s without, 226 MB), streams it in 0.5 s with 2 MB of memory, and converts values lazily.
Tests and tooling
Section titled “Tests and tooling”- a fixture corpus (RFC 5545 examples, Google, Apple, Outlook, Exchange, Nextcloud and Fastmail style calendars, broken input, regressions) with golden files
- property-based, fuzz and pathological input tests; differential tests against python-dateutil
- PHPStan level 8, PHP CS Fixer, CI jobs for tests, coding standard and differential tests