php 68 lines · 8 steps

Building a valid iCalendar feed in PHP in Laravel

A service class turns a booking into a spec-compliant .ics file, handling UTC timestamps, escaping, and line folding.

Explained by highlit
1<?php
2 
3namespace App\Services;
4 
5use App\Models\Booking;
6use DateTimeInterface;
7use DateTimeImmutable;
8use DateTimeZone;
9 
10class ICalendarExporter
11{
12 private const CRLF = "\r\n";
13 
14 public function forBooking(Booking $booking): string
15 {
16 $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
17 
18 $lines = [
19 'BEGIN:VCALENDAR',
20 'VERSION:2.0',
21 'PRODID:-//Acme Bookings//EN',
22 'CALSCALE:GREGORIAN',
23 'METHOD:PUBLISH',
24 'BEGIN:VEVENT',
25 'UID:' . $booking->reference . '@acme.test',
26 'DTSTAMP:' . $this->formatUtc($now),
27 'DTSTART:' . $this->formatUtc($booking->starts_at),
28 'DTEND:' . $this->formatUtc($booking->ends_at),
29 'SUMMARY:' . $this->escape($booking->title),
30 'DESCRIPTION:' . $this->escape($booking->notes ?? ''),
31 'LOCATION:' . $this->escape($booking->location),
32 'ORGANIZER;CN=' . $this->escape($booking->host->name) . ':mailto:' . $booking->host->email,
33 'STATUS:CONFIRMED',
34 'SEQUENCE:0',
35 'END:VEVENT',
36 'END:VCALENDAR',
37 ];
38 
39 return collect($lines)
40 ->map(fn (string $line): string => $this->fold($line))
41 ->implode(self::CRLF) . self::CRLF;
42 }
43 
44 private function formatUtc(DateTimeInterface $date): string
45 {
46 return DateTimeImmutable::createFromInterface($date)
47 ->setTimezone(new DateTimeZone('UTC'))
48 ->format('Ymd\\THis\\Z');
49 }
50 
51 private function escape(string $value): string
52 {
53 return str_replace(
54 ['\\', ';', ',', "\n"],
55 ['\\\\', '\\;', '\\,', '\\n'],
56 trim($value),
57 );
58 }
59 
60 private function fold(string $line): string
61 {
62 if (strlen($line) <= 75) {
63 return $line;
64 }
65 
66 return implode(self::CRLF . ' ', str_split($line, 74));
67 }
68}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1The iCalendar format demands strict rules — CRLF endings, UTC timestamps, escaped text, and 75-octet line folding — that a serializer must enforce.
  2. 2Isolating formatting concerns into small private helpers keeps the main assembly readable and each rule testable.
  3. 3Building output as an array of lines and joining at the end makes the structure obvious and easy to modify.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a valid iCalendar feed in PHP in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code