Aller au contenu

Position

phil/doctrine-behaviours
TypeGenreRôle
Contract\Positionableinterfaceposition(), placeAt(), positionGroup()
PositionableEntitytraitLa colonne position, un entier, nullable ; un seul ordre pour la table
PositionListenerécouteur : onFlushEnvoie un enregistrement sans position à la fin de son groupe
PositionsservicemoveBefore(), moveAfter(), moveFirst(), moveLast()
Exception\InvalidScopeexceptionUn groupe que l'écouteur ne sait pas comparer, ou un déplacement d'un groupe à l'autre
#[ORM\Entity]
#[ORM\Index(columns: ['menu_id', 'position'])]
class MenuItem implements Positionable
{
    use PositionableEntity;

    #[ORM\ManyToOne(targetEntity: Menu::class)]
    private ?Menu $menu = null;

    public function positionGroup(): array { return ['menu']; }
}
$positions = new Positions($manager);
$positions->moveBefore($item, $other);
$positions->moveLast($item);

$repository->findBy(['menu' => $menu], ['position' => 'ASC']);

Les positions ne font qu'ordonner. Un trou entre deux positions ne signifie rien, et un enregistrement retiré en laisse un : l'ordre des autres reste le même.

Le groupe

positionGroup() nomme les champs dont les valeurs forment un groupe ; chaque groupe est ordonné à part. Un champ est une colonne de l'enregistrement, colonne d'énumération comprise, ou une relation vers un seul côté qu'il tient par une colonne, comme un parent ou un locataire. Une valeur nulle est un groupe à elle seule, comparée par IS NULL. Le trait donne [] : un seul ordre pour toute la table.

Une relation vers plusieurs, une relation tenue par plusieurs colonnes, ou une relation vers un enregistrement dont l'identifiant est lui-même une relation lève InvalidScope au vidage qui en a besoin.

Quand une position est donnée

VidagePosition
un enregistrement inséré sans positionla fin de son groupe
un enregistrement inséré avec une positiongardée telle quelle
un enregistrement modifié après placeAt(null)la fin de son groupe
un enregistrement passé dans un autre groupe, sa position intactela fin de son nouveau groupe
un enregistrement passé dans un autre groupe avec une nouvelle positiongardée telle quelle
tout autre changementlaissée en paix

La fin d'un groupe est une seule requête, MAX(position), sur la table elle-même, pour que les lignes que cachent les filtres SQL comptent quand même. Les enregistrements d'un même vidage dans un même groupe se suivent ; un groupe dont la relation pointe vers un enregistrement du même vidage, pas encore enregistré, commence à 1. Une sous-classe compte la table qui porte la colonne.

Déplacer

Positions déplace un enregistrement enregistré dans son groupe, en une transaction sur la table elle-même :

MéthodeEffet
moveBefore($record, $other)$record prend la position de $other ; $other et les enregistrements qui le suivent reculent d'une place
moveAfter($record, $other)$record prend la place qui suit $other ; les enregistrements qui suivent reculent d'une place
moveFirst($record)$record prend la première position du groupe ; les autres reculent d'une place
moveLast($record)$record prend la place qui suit le dernier enregistrement du groupe

Chaque déplacement verrouille avant de lire, par une écriture qui ne change rien, ce qui tient sur chaque plateforme : moveBefore() et moveAfter() verrouillent la ligne de $other, moveFirst() et moveLast() chaque ligne du groupe. Deux déplacements vers la même extrémité d'un groupe s'attendent donc l'un l'autre, et le second lit la position que le premier a validée, au lieu que les deux prennent la même place ; des déplacements dans des groupes différents ne s'attendent pas. Un déplacement vers une extrémité écrit une fois chaque ligne de son groupe, ce que l'index ci-dessous limite au groupe.

Les écouteurs ne s'exécutent pas, et les lignes supprimées en douceur, désactivées ou filtrées se déplacent avec les autres. Un enregistrement placé contre lui-même, ou seul dans son groupe, reste où il est. Un enregistrement placé contre un enregistrement d'un autre groupe ou d'une autre table, un enregistrement pas encore enregistré, et un enregistrement dont le groupe pointe vers un enregistrement pas encore enregistré lèvent InvalidScope : videz d'abord.

Les enregistrements chargés du groupe suivent le déplacement en mémoire, si bien que le vidage suivant n'a rien à écrire. Un enregistrement chargé dont la position a été changée sans être vidée garde ce changement, que le vidage suivant écrit. Videz un changement de groupe avant de déplacer l'enregistrement : le déplacement lit le groupe dans l'enregistrement en mémoire.

Index

L'application déclare un index simple sur les colonnes du groupe et position, pour le tri et pour MAX(position). Un index unique ne convient pas : un déplacement décale une plage de positions en une seule instruction, ce qu'un index unique peut refuser à mi-chemin sur certaines plateformes.