How to Parse iCalendar (ICS) Links in PHP for Availability or Scheduling

How_to_Parse_iCalendar_%28ICS%29_Links_in_PHP_for_Availability_or_Scheduling_-_Thumbnail.png

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:

  1. The structure of iCalendar (.ics) files and their quirks
  2. A reliable PHP approach to extract date ranges from events
  3. How to normalize, parse, and convert ICS lines into a list of dates
  4. Best practices for processing large calendars and handling errors
  5. 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 and DTEND. 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.