Sincronizzazione iCal con Airbnb e Booking: problemi e soluzioni
Come ho implementato la sincronizzazione bidirezionale iCal tra un booking engine PHP custom e i portali OTA, i bug incontrati e le soluzioni adottate.
Il problema: prenotazioni doppie tra Airbnb e sito diretto
Il cliente ha un B&B su Airbnb e Booking.com, ma vuole anche prenotazioni dirette dal suo sito — senza commissioni OTA (che arrivano al 15-18%). Il problema classico: se qualcuno prenota su Airbnb, la stessa data deve risultare occupata sul sito, e viceversa. In tempo reale.
La soluzione standard è iCal — un formato di calendario (.ics) che tutti i portali supportano per l'import/export delle disponibilità. Suona semplice. Non lo è.
Come funziona iCal in pratica
Il protocollo è pull-based: ogni portale scarica periodicamente un URL .ics dal tuo server e aggiorna le sue disponibilità. Non è un webhook. Non è real-time. Airbnb fa il pull ogni 3 ore, Booking.com ogni 2-4 ore.
// Generazione feed iCal per una proprietà
function generateICalFeed($propertyId) {
global $conn;
$stmt = $conn->prepare("SELECT * FROM bookings
WHERE property_id = ? AND status IN ('confirmed','checked_in')
AND check_out >= CURDATE()");
$stmt->bind_param("i", $propertyId);
$stmt->execute();
$bookings = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$ical = "BEGIN:VCALENDAR\r\n";
$ical .= "VERSION:2.0\r\n";
$ical .= "PRODID:-//BookingManager//IT\r\n";
$ical .= "CALSCALE:GREGORIAN\r\n";
$ical .= "METHOD:PUBLISH\r\n";
foreach ($bookings as $b) {
$uid = 'bm-' . $b['id'] . '@' . $_SERVER['HTTP_HOST'];
$ical .= "BEGIN:VEVENT\r\n";
$ical .= "UID:" . $uid . "\r\n";
$ical .= "DTSTART;VALUE=DATE:" . str_replace('-', '', $b['check_in']) . "\r\n";
$ical .= "DTEND;VALUE=DATE:" . str_replace('-', '', $b['check_out']) . "\r\n";
$ical .= "SUMMARY:Prenotato - " . htmlspecialchars($b['guest_name']) . "\r\n";
$ical .= "STATUS:CONFIRMED\r\n";
$ical .= "END:VEVENT\r\n";
}
$ical .= "END:VCALENDAR\r\n";
return $ical;
}
Il parsing dei feed esterni: dove iniziano i problemi
Importare i feed iCal da Airbnb e Booking sembra banale — è un file di testo strutturato. Ma ogni portale ha le sue "personalità":
- Airbnb usa
DTSTART;VALUE=DATE:20260415(solo data, senza orario). Le prenotazioni includono anche i "blocked dates" che il proprietario ha impostato manualmente. - Booking.com a volte include l'orario:
DTSTART:20260415T150000Z. E ilSUMMARYcontiene il nome dell'ospite in formati diversi a seconda della lingua del portale. - Entrambi possono avere eventi sovrapposti che non corrispondono a prenotazioni reali ma a "buffer days" o blocchi manuali.
// Parser iCal robusto: gestisce formati data diversi
function parseICalDate($dateStr) {
$dateStr = trim($dateStr);
// Formato 1: 20260415 (solo data)
if (preg_match('/^(\d{8})$/', $dateStr, $m)) {
return substr($m[1], 0, 4) . '-' . substr($m[1], 4, 2) . '-' . substr($m[1], 6, 2);
}
// Formato 2: 20260415T150000Z (con orario UTC)
if (preg_match('/^(\d{8})T/', $dateStr, $m)) {
return substr($m[1], 0, 4) . '-' . substr($m[1], 4, 2) . '-' . substr($m[1], 6, 2);
}
// Fallback: prova DateTime
try {
$dt = new DateTime($dateStr);
return $dt->format('Y-m-d');
} catch (Exception $e) {
return null; // Data non parsabile — logga e ignora
}
}
Mai fidarsi del formato di un feed esterno. Ogni portale implementa iCal "a modo suo". Il parser deve essere difensivo: gestire tutti i formati noti, loggare quelli sconosciuti, e non crashare mai.
Il cron job che non c'è: sync su Aruba senza crontab
Su Aruba shared hosting non hai accesso a crontab. La sincronizzazione periodica dei feed esterni è gestita con un pseudo-cron PHP: ogni volta che qualcuno visita il sito, il sistema controlla se sono passate più di 2 ore dall'ultimo sync. Se sì, scarica i feed in background.
// Pseudo-cron: sync iCal al primo visitatore dopo 2 ore
function maybeSyncFeeds($propertyId) {
$lockFile = __DIR__ . '/tmp/sync_' . $propertyId . '.lock';
$lastSync = file_exists($lockFile) ? filemtime($lockFile) : 0;
if (time() - $lastSync < 7200) return; // meno di 2 ore: skip
touch($lockFile); // lock immediato per evitare sync paralleli
// Scarica e parsa i feed in background
$feeds = getFeedUrls($propertyId); // Airbnb URL, Booking URL
foreach ($feeds as $feed) {
$ics = @file_get_contents($feed['url']);
if ($ics) {
$events = parseICal($ics);
syncEventsToDb($propertyId, $events, $feed['source']);
}
}
}
Su hosting condivisi senza SSH, file_get_contents() per URL esterni è spesso disabilitato. Il fallback è cURL — che su Aruba funziona sempre. Tutto il networking del progetto passa da cURL.
Da v1 a v2: il refactor completo
HotelManager v1 usava SQLite e gestiva una singola proprietà. BookingManager v2 è un rewrite completo: MySQL, multi-proprietà, Stripe+Klarna, PayPal Checkout API v2, e un sistema di report fiscali mensili. Il refactor ha toccato 37 file — praticamente tutto.
La lezione più grande del refactor: non fare refactor incrementali su codice monolitico. È più veloce e pulito riscrivere da zero con l'architettura giusta, riutilizzando solo le query SQL e la logica business testata.