From 23cccf170ac6f29d3f60d1a0dc34667da06aaaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Wed, 4 Nov 2026 17:24:50 +0100 Subject: [PATCH] Unique and stable event aliases from the frontend form The frontend form built the alias from date + first 20 title characters without checking for existing aliases and recalculated it on every save. Duplicating an event and saving the copy with the same title and date (or entering an event twice) produced the same alias, so only one of the events was reachable. - aliases are made unique (-2, -3, ...) - existing events keep their alias on update (stable URLs); only an empty alias, e.g. of a fresh copy, is generated - migration renames existing duplicates; the oldest event keeps its alias and URL Co-Authored-By: Claude Opus 5.5 --- .../DuplicateEventAliasMigration.php | 76 +++++++++++++++++++ src/Service/EventRepository.php | 53 ++++++++++++- 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/Migration/DuplicateEventAliasMigration.php diff --git a/src/Migration/DuplicateEventAliasMigration.php b/src/Migration/DuplicateEventAliasMigration.php new file mode 100644 index 0000000..acfa474 --- /dev/null +++ b/src/Migration/DuplicateEventAliasMigration.php @@ -0,0 +1,76 @@ +connection->createSchemaManager()->tablesExist(['tl_calendar_events'])) { + return false; + } + + return [] !== $this->findDuplicateAliases(); + } + + public function run(): MigrationResult + { + $renamed = []; + + foreach ($this->findDuplicateAliases() as $alias) { + $ids = $this->connection->fetchFirstColumn('SELECT id FROM tl_calendar_events WHERE alias = ? ORDER BY id', [$alias]); + + // ältestes Event behält den Alias + array_shift($ids); + $counter = 2; + + foreach ($ids as $id) { + do { + $candidate = $alias.'-'.$counter++; + } while (false !== $this->connection->fetchOne('SELECT id FROM tl_calendar_events WHERE alias = ?', [$candidate])); + + $this->connection->update('tl_calendar_events', ['alias' => $candidate, 'tstamp' => time()], ['id' => (int) $id]); + $renamed[] = sprintf('ID %d: %s', $id, $candidate); + } + } + + return $this->createResult(true, 'Umbenannt: '.implode(', ', $renamed)); + } + + /** + * @return list + */ + private function findDuplicateAliases(): array + { + return $this->connection->fetchFirstColumn( + "SELECT alias FROM tl_calendar_events WHERE alias != '' GROUP BY alias HAVING COUNT(*) > 1", + ); + } +} diff --git a/src/Service/EventRepository.php b/src/Service/EventRepository.php index 6e28952..265fcc0 100644 --- a/src/Service/EventRepository.php +++ b/src/Service/EventRepository.php @@ -526,7 +526,7 @@ class EventRepository */ public function update(int $eventId, array $data): void { - $eventData = $this->buildEventData($data); + $eventData = $this->buildEventData($data, $eventId); $this->connection->update( 'tl_calendar_events', @@ -631,13 +631,13 @@ class EventRepository * * @return array */ - private function buildEventData(array $data): array + private function buildEventData(array $data, ?int $eventId = null): array { $startDate = !empty($data['startDate']) ? strtotime((string) $data['startDate']) : false; $endDate = !empty($data['endDate']) ? strtotime((string) $data['endDate']) : false; $url = trim((string) ($data['url'] ?? '')); $addTime = !empty($data['addTime']); - $alias = $this->buildAlias($startDate, (string) ($data['title'] ?? '')); + $alias = $this->resolveAlias($startDate, (string) ($data['title'] ?? ''), $eventId); $startDateTimestamp = false !== $startDate ? (int) $startDate : 0; $endDateTimestamp = false !== $endDate ? (int) $endDate : 0; @@ -678,6 +678,53 @@ class EventRepository ]; } + /** + * Alias für ein Event aus dem Frontend-Formular. + * + * - Bestehende Events behalten ihren Alias (stabile URLs, auch wenn Titel oder + * Datum korrigiert werden); nur ein leerer Alias wird neu erzeugt, z. B. bei + * einer frisch duplizierten Kopie. + * - Der Alias ist immer eindeutig: belegte Aliasse bekommen -2, -3, ... angehängt. + */ + private function resolveAlias(false|int $startDate, string $title, ?int $eventId): string + { + if (null !== $eventId && $eventId > 0) { + $existing = (string) $this->connection->fetchOne('SELECT alias FROM tl_calendar_events WHERE id = ?', [$eventId]); + + if ('' !== $existing && !$this->aliasExists($existing, $eventId)) { + return $existing; + } + } + + $base = $this->buildAlias($startDate, $title); + + if ('' === $base) { + return ''; + } + + return $this->makeUniqueAlias($base, $eventId); + } + + private function makeUniqueAlias(string $base, ?int $eventId = null): string + { + $candidate = $base; + $counter = 2; + + while ($this->aliasExists($candidate, $eventId)) { + $candidate = $base.'-'.$counter++; + } + + return $candidate; + } + + private function aliasExists(string $alias, ?int $exceptEventId = null): bool + { + return false !== $this->connection->fetchOne( + 'SELECT id FROM tl_calendar_events WHERE alias = ? AND id != ? LIMIT 1', + [$alias, (int) $exceptEventId], + ); + } + private function buildAlias(false|int $startDate, string $title): string { if (false === $startDate || $startDate <= 0) {