213 lines
6.3 KiB
PHP
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';
|
||
|
|
}
|
||
|
|
}
|