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 <noreply@anthropic.com>
This commit is contained in:
1 parent
7df08b9017
commit
23cccf170a
2 files changed
+126
-3
No files matched your search
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\EventManagerBundle\Migration;
|
||||
|
||||
use Contao\CoreBundle\Migration\AbstractMigration;
|
||||
use Contao\CoreBundle\Migration\MigrationResult;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
/**
|
||||
* Doppelte Event-Aliasse auflösen.
|
||||
*
|
||||
* Das Frontend-Formular hat Aliasse früher ohne Eindeutigkeitsprüfung aus Datum und
|
||||
* Titel gebaut. Bei gleichem Titel und Datum (z. B. nach dem Duplizieren) entstand
|
||||
* derselbe Alias, und nur eines der Events war über seine URL erreichbar.
|
||||
*
|
||||
* Pro Gruppe behält das älteste Event (niedrigste ID) seinen Alias und damit seine
|
||||
* bisherige URL, die übrigen bekommen -2, -3, ... angehängt. Leere Aliasse (Entwürfe)
|
||||
* bleiben unberührt, Contao verwendet dort die ID.
|
||||
*/
|
||||
class DuplicateEventAliasMigration extends AbstractMigration
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Eventmanager: doppelte Event-Aliasse eindeutig machen';
|
||||
}
|
||||
|
||||
public function shouldRun(): bool
|
||||
{
|
||||
if (!$this->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<string>
|
||||
*/
|
||||
private function findDuplicateAliases(): array
|
||||
{
|
||||
return $this->connection->fetchFirstColumn(
|
||||
"SELECT alias FROM tl_calendar_events WHERE alias != '' GROUP BY alias HAVING COUNT(*) > 1",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<string, mixed>
|
||||
*/
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user