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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1The iCalendar format demands strict rules — CRLF endings, UTC timestamps, escaped text, and 75-octet line folding — that a serializer must enforce.
- 2Isolating formatting concerns into small private helpers keeps the main assembly readable and each rule testable.
- 3Building output as an array of lines and joining at the end makes the structure obvious and easy to modify.
Related explainers
php
<?php namespace App\Http\Requests\DataObjects;
Typed request DTOs in Laravel
data-transfer-object
validation
immutability
Intermediate
6 steps
php
<?php namespace App\Jobs;
Rendering invoice PDFs in a Laravel queue job
queued jobs
pdf generation
file storage
Intermediate
9 steps
java
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule;
Serializing Money to JSON in Spring
serialization
jackson
money
Intermediate
8 steps
php
<?php namespace App\View\Components;
Building a breadcrumb component in Laravel
blade components
url parsing
string manipulation
Intermediate
8 steps
rust
pub fn format_size(bytes: u64) -> String { const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; if bytes < 1024 {
Formatting byte counts as human-readable sizes
bit-manipulation
formatting
unit-conversion
Intermediate
5 steps
php
<?php namespace App\Experiments;
Weighted random selection in PHP
weighted-random
cumulative-sum
sampling
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-valid-icalendar-feed-in-php-in-laravel-explained-php-bd01/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.