How to Parse iCalendar (ICS) Links in PHP for Availability or Scheduling
Parsing .ics
files from iCalendar feeds is a common requirement when dealing with booking systems, calendar integrations, or availability logic. These files are often used to share calendar data across platforms, such as marking unavailable dates or scheduled events. But working directly with raw ICS data requires careful handling to avoid common pitfalls in the format.
In this guide, we’ll cover:
- The structure of iCalendar (.ics) files and their quirks
- A reliable PHP approach to extract date ranges from events
- How to normalize, parse, and convert ICS lines into a list of dates
- Best practices for processing large calendars and handling errors
- Frequently asked questions about iCal parsing
By the end, you’ll have a lightweight and dependable parser that can convert .ics
files into usable date arrays for any custom application.
Why iCal Files Aren’t as Simple as They Look
A typical .ics
file might contain event blocks like:
BEGIN:VEVENT
DTSTART;VALUE=DATE:20250701
DTEND;VALUE=DATE:20250703
END:VEVENT
But several formatting rules make it more complicated than a straightforward line-by-line parse:
Gotcha | Explanation |
---|---|
Line folding | Long lines can be split across multiple lines and must be rejoined. |
Exclusive DTEND | For all-day events, the DTEND date is typically exclusive (the day after the last day). |
Time zones and timestamps | Events may be expressed in local time, UTC, or with timezone identifiers. |
Missing or malformed dates | Some events may be incomplete or invalid. |
Large file sizes | A calendar may contain hundreds or thousands of events. |
For most use cases—like extracting unavailable or scheduled dates—the goal is to extract the start and end dates of each event and expand them into a usable array of discrete dates.
Step 1: Normalize Line Endings and Unfold Wrapped Lines
function normalize_ics_lines(string $ics): array {
$ics = preg_replace("/\r\n[ \t]/", '', $ics); // Unfold lines
$ics = str_replace(["\r\n", "\r"], "\n", $ics); // Normalize line endings
return array_filter(array_map('trim', explode("\n", $ics)));
}
Step 2: Extract VEVENT Blocks
function extract_vevent_blocks(array $lines): array {
$events = [];
$inEvent = false;
$current = [];
foreach ($lines as $line) {
if (strpos($line, 'BEGIN:VEVENT') === 0) {
$inEvent = true;
$current = [];
continue;
}
if (strpos($line, 'END:VEVENT') === 0) {
if (isset($current['DTSTART'], $current['DTEND'])) {
$events[] = $current;
}
$inEvent = false;
continue;
}
if ($inEvent) {
if (strpos($line, 'DTSTART') === 0) {
$current['DTSTART'] = $line;
} elseif (strpos($line, 'DTEND') === 0) {
$current['DTEND'] = $line;
}
}
}
return $events;
}
Step 3: Expand Date Ranges into Individual Days
function expand_event_dates(array $event): array {
preg_match('/:(\d{8})/', $event['DTSTART'], $startMatch);
preg_match('/:(\d{8})/', $event['DTEND'], $endMatch);
if (empty($startMatch[1]) || empty($endMatch[1])) {
return [];
}
$start = DateTime::createFromFormat('Ymd', $startMatch[1]);
$end = DateTime::createFromFormat('Ymd', $endMatch[1]);
if (!$start || !$end) {
return [];
}
$interval = new DateInterval('P1D');
$end->modify('+1 day'); // Ensure DTEND (exclusive) is included
$range = new DatePeriod($start, $interval, $end);
$dates = [];
foreach ($range as $date) {
$dates[] = $date->format('Y-m-d');
}
return $dates;
}
Step 4: Full Parser to Extract All Dates
function parse_ics_calendar(string $icsContent): array {
$lines = normalize_ics_lines($icsContent);
$events = extract_vevent_blocks($lines);
$allDates = [];
foreach ($events as $event) {
$allDates = array_merge($allDates, expand_event_dates($event));
}
$allDates = array_unique($allDates);
sort($allDates);
return [
'dates' => $allDates
];
}
Example Usage
$response = wp_remote_get('https://example.com/calendar.ics');
if (!is_wp_error($response)) {
$ics = wp_remote_retrieve_body($response);
$parsed = parse_ics_calendar($ics);
// Use $parsed['dates'] as needed
foreach ($parsed['dates'] as $date) {
echo $date . "\n";
}
}
This can easily be adapted for any context—displaying on a front end, storing in a database, comparing to current availability, or flagging overlaps.
Best Practices and Considerations
- Error handling: Always verify that each event has a valid
DTSTART
andDTEND
. Log or skip otherwise. - Memory limits: For very large calendars, consider processing events in batches or using streaming techniques.
- Time zone handling: This example assumes all-day events using the
YYYYMMDD
format. Parsing timed events with time zone support requires additional logic. - Date formats: The output format
'Y-m-d'
is universally usable for comparisons and storage, but you can adjust formatting as needed.
FAQ
Can this handle recurring events?
Not by default. If you need to handle recurrence rules (RRULE
), consider using a third-party library that supports recurrence expansion.
What happens if DTEND is before DTSTART?
These events are ignored in the example above. You may wish to validate and skip or log them.
Can I convert these dates into a calendar view?
Yes, the output array of Y-m-d
strings can be fed into any JavaScript calendar library or custom UI component.
Are partial-day events supported?
The example targets full-day spans. To support time-based events (T
format), you’d need to parse DTSTART:YYYYMMDDTHHmmSS
and include time handling.
Conclusion
ICS parsing may seem trivial at first, but it hides subtle formatting and logic rules that must be handled carefully. With a reliable parser in place, you can convert iCal feeds into simple date arrays suitable for availability checks, booking rules, scheduling engines, or UI calendars.
This approach is compact, dependency-free, and easy to adapt to any use case. Whether you’re syncing calendars, preventing overlaps, or building custom interfaces, parsing ICS data with precision gives you control over how and when things are booked or shown.