Files
glastree/app/Services/IcsExportService.php
T

213 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Evento;
use Carbon\Carbon;
class IcsExportService
{
private const DATETIME_FORMAT = 'Ymd\THis';
private const DATE_FORMAT = 'Ymd';
private const TIMEZONE = 'Europe/Rome';
private const DAY_MAP = [
0 => 'SU',
1 => 'MO',
2 => 'TU',
3 => 'WE',
4 => 'TH',
5 => 'FR',
6 => 'SA',
];
public function generateSingle(Evento $evento): string
{
return $this->generateCollection(collect([$evento]));
}
public function generateCollection(iterable $eventi): string
{
$lines = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Glastree//Eventi//IT',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'X-WR-CALNAME:' . config('app.name') . ' - Eventi',
];
foreach ($eventi as $evento) {
array_push($lines, ...$this->buildVEvent($evento));
}
$lines[] = 'END:VCALENDAR';
return $this->fold(implode("\r\n", $lines));
}
private function buildVEvent(Evento $evento): array
{
$uid = 'evento-' . $evento->id . '@' . preg_replace('/[^a-z0-9]/', '', strtolower(config('app.name')));
$lines = [
'BEGIN:VEVENT',
'UID:' . $uid,
'DTSTAMP:' . now()->format(self::DATETIME_FORMAT) . 'Z',
];
if ($evento->isSingolo() && $evento->data_specifica) {
$date = $evento->data_specifica instanceof Carbon
? $evento->data_specifica
: Carbon::parse($evento->data_specifica);
if ($evento->ora_inizio) {
$start = $date->copy()->setTimeFrom($evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio));
$durata = $evento->durata_minuti ?? 60;
$end = $start->copy()->addMinutes((int) $durata);
$lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $start->format(self::DATETIME_FORMAT);
$lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $end->format(self::DATETIME_FORMAT);
} else {
$lines[] = 'DTSTART;VALUE=DATE:' . $date->format(self::DATE_FORMAT);
$lines[] = 'DTEND;VALUE=DATE:' . $date->copy()->addDay()->format(self::DATE_FORMAT);
}
} else {
$rrule = $this->buildRRule($evento);
if ($rrule) {
$lines[] = $rrule;
}
if ($evento->ora_inizio) {
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
$lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $time->format('His');
$durata = $evento->durata_minuti ?? 60;
$endTime = $time->copy()->addMinutes((int) $durata);
$lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $endTime->format('His');
}
}
$lines[] = 'SUMMARY:' . $this->escapeText($evento->nome_evento);
if ($evento->descrizione_evento || $evento->descrizione) {
$desc = $evento->descrizione_evento
? ($evento->descrizione ? $evento->descrizione_evento . '\n\n' . $evento->descrizione : $evento->descrizione_evento)
: $evento->descrizione;
$lines[] = 'DESCRIPTION:' . $this->escapeText($desc);
}
if ($evento->luogo_indirizzo) {
$lines[] = 'LOCATION:' . $this->escapeText($evento->luogo_indirizzo);
}
$lines[] = 'END:VEVENT';
return $lines;
}
private function buildRRule(Evento $evento): ?string
{
return match ($evento->tipo_recorrenza) {
'settimanale' => $this->rruleSettimanale($evento),
'mensile' => $this->rruleMensile($evento),
'annuale' => $this->rruleAnnuale($evento),
'altro' => $this->rruleAltro($evento),
default => null,
};
}
private function rruleSettimanale(Evento $evento): ?string
{
if ($evento->giorno_settimana === null) {
return null;
}
return 'RRULE:FREQ=WEEKLY;BYDAY=' . (self::DAY_MAP[(int) $evento->giorno_settimana] ?? 'MO');
}
private function rruleMensile(Evento $evento): ?string
{
if (!$evento->occorrenza_mese) {
return null;
}
$parts = explode(',', $evento->occorrenza_mese);
if (count($parts) !== 2) {
return null;
}
$occorrenza = (int) $parts[0];
$giorno = (int) $parts[1];
$byDay = self::DAY_MAP[$giorno] ?? 'MO';
$rrule = 'RRULE:FREQ=MONTHLY;BYSETPOS=' . $occorrenza . ';BYDAY=' . $byDay;
if ($evento->mesi_recorrenza) {
$mesi = explode(',', $evento->mesi_recorrenza);
$rrule .= ';BYMONTH=' . implode(',', $mesi);
}
return $rrule;
}
private function rruleAnnuale(Evento $evento): ?string
{
if (!$evento->mese_annuale) {
return null;
}
$rrule = 'RRULE:FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale;
$giorno = (int) ($evento->giorno_mese ?? 1);
$rrule .= ';BYMONTHDAY=' . $giorno;
return $rrule;
}
private function rruleAltro(Evento $evento): ?string
{
return $this->rruleSettimanale($evento);
}
private function escapeText(?string $text): string
{
if ($text === null) {
return '';
}
$text = str_replace(
['\\', ';', ',', "\n"],
['\\\\', '\\;', '\\,', '\\n'],
$text
);
return $text;
}
private function fold(string $ics): string
{
$lines = explode("\r\n", $ics);
$folded = [];
foreach ($lines as $line) {
while (mb_strlen($line) > 75) {
$folded[] = mb_substr($line, 0, 75);
$line = ' ' . mb_substr($line, 75);
}
$folded[] = $line;
}
return implode("\r\n", $folded);
}
public function getFilename(Evento $evento): string
{
$name = str_replace(['/', '\\', ' '], '_', $evento->nome_evento);
return $name . '.ics';
}
public function getCollectionFilename(): string
{
return 'eventi_' . now()->format('Ymd_His') . '.ics';
}
}