Aller au contenu

Historique des anciens slugs

phil/slug-bundle

Quand un titre change, son slug change souvent avec lui, et chaque lien vers l'ancienne adresse se brise. L'historique garde les anciens slugs de chaque sujet, chacun pointant vers le slug actuel du sujet, pour qu'une ancienne adresse reçoive une redirection permanente plutôt qu'une 404. Il est facultatif et désactivé par défaut : il demande doctrine/dbal et une table.

L'activer

composer require doctrine/dbal
# config/packages/phil_slug.yaml
phil_slug:
    history:
        enabled: true
        connection: default   # the DBAL connection of the table
        clock: clock          # the PSR-20 clock of the recorded changes

history: true abrège enabled: true avec les valeurs par défaut. Quand doctrine/orm est installé, la table phil_slug_history est ajoutée au schéma que l'ORM génère pour cette connexion : doctrine:migrations:diff écrit alors la migration qui la crée, et doctrine:schema:validate ne demande pas de la supprimer. Sans l'ORM, créez-la à partir de SlugHistoryTable::configure().

L'historique est le service phil_slug.history, autoconnecté comme Phil\SlugBundle\Application\Contract\SlugHistory.

Portées et sujets

MotCe que c'estRègle
PortéeL'espace dans lequel un slug est unique : page, page:fr, tenant-42:productDe 1 à 100 octets de lettres ASCII, de chiffres et de _ . : / -
SujetL'identifiant de ce que le slug nomme dans sa portée, tel que l'application l'écrit : 42, un UUIDDe 1 à 191 octets de texte UTF-8 sans caractère de contrôle

Une portée ou un sujet hors de ces règles est refusé avec InvalidSlugHistory avant toute requête, plutôt que coupé pour tenir dans sa colonne. Une page traduite enregistre ses changements dans une portée par langue, et un site à plusieurs locataires met le locataire dans la portée.

Enregistrer un changement

Le code qui change un slug enregistre le changement, dans la même unité de travail :

use Phil\Slug\Slug;
use Phil\SlugBundle\Application\Contract\SlugHistory;

final readonly class RenamePage
{
    public function __construct(private SlugHistory $history) {}

    public function __invoke(Page $page, Slug $newSlug): void
    {
        $this->history->record('page:'.$page->locale, (string) $page->id, $page->slug, $newSlug);
        $page->slug = $newSlug;
    }
}

Un changement s'écrit en une transaction de trois instructions, si bien que l'historique ne garde jamais de chaîne :

  1. le nouveau slug cesse d'être un ancien slug de la portée, quel que soit le sujet qui l'avait : une adresse de nouveau en usage n'est plus redirigée ;
  2. chaque ancien slug du sujet pointe désormais vers le nouveau slug ;
  3. l'ancien slug est écrit comme ancien slug du sujet, à la place de toute ligne qu'il avait.
Changements du sujet 7 dans pageAnciens slugs ensuite
a → ba → b
puis b → ca → c, b → c
puis c → ab → a, c → a

Enregistrer un slug comme son propre ancien slug ne change rien. L'historique grandit d'au plus une ligne par ancien slug distinct d'un sujet, quel que soit le nombre de changements.

Le lire

MéthodeRetourne
currentSlug(string $scope, Slug $former): ?SlugLe slug actuel du sujet qui a eu $former, ou null
formerSlugs(string $scope): iterable<string, Slug>Chaque ancien slug de la portée, en clé, avec le slug actuel vers lequel il pointe, dans l'ordre des anciens slugs
forget(string $scope, string $subject): voidRetire chaque ancien slug du sujet : il a été supprimé, ou ses anciennes adresses ne doivent plus répondre

Un contrôleur qui ne trouve aucune page pour un slug peut demander currentSlug() et rediriger avec un 301 ; un fournisseur de redirections lit formerSlugs() pour bâtir sa table. Une ligne modifiée à la main en quelque chose qui n'est pas un slug ne reçoit pas de réponse.

L'historique ne garde rien entre deux appels : il est sûr dans un processus de longue durée.