tirets. * Toujours non vide (retourne "item" en dernier recours). */ public static function slugify(string $text): string { $text = trim($text); if ($text === '') { return 'item'; } // Transliteration intl si dispo (cas de Marseille -> marseille, é -> e). if (class_exists(\Transliterator::class)) { $tr = \Transliterator::create('Any-Latin; Latin-ASCII; Lower()'); if ($tr !== null) { $translit = $tr->transliterate($text); if (is_string($translit)) { $text = $translit; } } } $text = mb_strtolower($text); // Remplace toute séquence non-[a-z0-9] par un tiret, puis trim les tirets de bord. $text = preg_replace('/[^a-z0-9]+/', '-', $text) ?? ''; $text = trim($text, '-'); return $text === '' ? 'item' : $text; } /** * Garantit l'unicité d'un slug dans une table : suffixe `-2`, `-3`, … * Si `$excludeId` est fourni, ignore cette ligne (cas PATCH d'un slug existant). */ public static function unique(string $base, string $table, string $column = 'slug', ?int $excludeId = null): string { $candidate = $base; $n = 2; while (self::exists($candidate, $table, $column, $excludeId)) { $candidate = $base . '-' . $n; $n++; } return $candidate; } private static function exists(string $slug, string $table, string $column, ?int $excludeId): bool { // Noms de table/colonne câblés depuis le code, jamais depuis l'utilisateur. $sql = sprintf('SELECT 1 FROM `%s` WHERE `%s` = :slug', $table, $column); $params = [':slug' => $slug]; if ($excludeId !== null) { $sql .= ' AND id <> :id'; $params[':id'] = $excludeId; } $sql .= ' LIMIT 1'; return Database::fetchValue($sql, $params) !== null; } }