Aller au contenu

Fiches

phil/directory-bundle

Une fiche est une racine de contenu de phil/content-bundle, avec une ligne par version, par étape et par locale. Ses champs sont les capacités de content-bundle — titre et description de l'extrait, slug, corps en Markdown, flux de travail, référencement — et cinq capacités du répertoire :

CapacitéClé des donnéesColonne
Contactcontact : le tableau de ContactCardArraycontact (phil_contact)
Adresseaddress : le tableau de AddressArrayaddress (phil_address)
Positionlocation : "45.5,-73.6"location_latitude, location_longitude
Liensrelated : des identifiants de fiches par relation, {"played_at": ["0190…"]}related (JSON)
Auteur de la versioneditedBy : l'identifiant de l'utilisateuredited_by

Qui peut faire quoi

ActionQuiAttribut de voter
Rédiger une fichetoute personne connectée, qui en devient l'auteur—
La modifierson auteur, ses rédacteurs, les administrateursphil_directory.edit
Nommer ou retirer des rédacteursson auteur, les administrateursphil_directory.editors
Décider qui la trouveson auteur, les administrateursphil_directory.editors
La publierles administrateursphil_directory.publish
La liretout le monde, une fois publiée—

Un rédacteur est un identifiant d'utilisateur de 1 à 255 octets ; une fiche en a jusqu'à 50. Nommer l'auteur ou un rédacteur deux fois ne change rien.

use Phil\DirectoryBundle\Application\Actor;
use Phil\DirectoryBundle\Application\Directory;

$reference = $directory->create('podcast', 'fr', ['excerptTitle' => 'Le Gnome', 'slug' => 'le-gnome'], new Actor('ada'));
$directory->addEditor($reference, 'grace', new Actor('ada'));
$directory->edit($reference, 'fr', ['excerptDescription' => 'Un balado.'], new Actor('grace'));
$directory->publish($reference, 'fr', new Actor('admin', admin: true));

Une action refusée lève ListingRefused, une DomainError ; les refus propres à content-bundle, comme un slug déjà pris, lèvent InvalidContent. Le contrôleur affiche les deux comme des erreurs de formulaire.

Fiches liées

related contient les fiches qu'une fiche nomme, selon les relations de son genre (configuration). Lorsqu'il est fourni, il remplace tout l'ensemble ; null le vide, et son absence le conserve. Directory le vérifie avant d'écrire :

  • chaque relation s'écrit sur le genre de la fiche — une autre est un ListingRefused ;
  • chaque fiche existe, est du genre to de la relation, et n'est pas la fiche elle-même ;
  • une fiche au plus pour une relation qui n'est pas multiple ;
  • une relation que la configuration ne déclare plus est écartée.
$directory->create('larp', 'fr', ['excerptTitle' => 'La Saga', 'related' => ['played_at' => [$site->resourceId]]], $admin);

foreach ($directory->related($site, 'fr') as $group) {
    $group->title();     // "Larps played here": the inverse title, on the larp site
    $group->listings;    // the published larps that name the site, by slug
}

related() donne les fiches publiées qu'une fiche nomme, puis celles qui la nomment, un RelatedListings par relation et par sens, en laissant de côté ceux qui n'en ont aucune. Les relations propres à la fiche sont lues au moment de servir sa page, le long de la chaîne de repli de la locale ; les fiches montrées sont celles qui sont publiées dans la locale avec un slug, comme dans la liste d'un genre, lues par leurs identifiants en une seule requête. Celles qui la nomment sont recherchées d'abord (RelatedLookup, dont l'adaptateur Doctrine est DoctrineRelatedLookup) : l'identifiant et la colonne related des lignes actives de l'autre genre qui en ont, lus comme tableaux sans hydrater d'entité, leur JSON étant parcouru en PHP — aucune requête portable ne cherche dans une colonne JSON. Seules ces fiches sont ensuite lues comme fiches publiées, par leurs identifiants, et conservées lorsque leur ligne publiée nomme la fiche : les fiches hydratées suivent les fiches liées, et non la taille du genre. relationsFrom($genre) donne les relations qu'offre le formulaire d'un genre.

La liste d'un genre

Directory::published($genre, $locale, $offset = 0, $limit = null, $reader = Reader::SiteListings) donne les fiches d'un genre publiées dans une locale, par slug, en une seule lecture des lignes publiées de content-bundle (PublishedRowQuery, filtré sur la colonne du genre et sur qui trouve la fiche pour le Reader donné), au besoin une page de celles-ci. Une fiche publiée sans slug n'a pas de page vers laquelle faire un lien : elle est laissée de côté dans la liste, dans les fiches liées et dans le plan du site.

Qui trouve une fiche

L'auteur d'une fiche décide qui la trouve, avec l'Indexing de phil/indexing-consent : Listing implémente HasIndexingConsent. Le choix se fait sur la page de ses rédacteurs, et prend effet tout de suite, sans attendre une publication : il appartient à la fiche, pas à une version.

ChoixLa liste de son genre, les fiches liéesLe plan du siteSa page
Listed, par défautmontréelistéeindexée
NotIndexedmontréelaissée de côténoindex, noarchive
Unlistedlaissée de côtélaissée de côténoindex, noarchive

La page d'une fiche qui n'est pas listée dit noindex, noarchive aux robots, dans son en-tête X-Robots-Tag et sa <meta name="robots"> ; elle reste joignable à son adresse, pour que les robots lisent la directive. La page porte le choix sur sa requête sous IndexingPolicy::REQUEST_ATTRIBUTE, et phil/structured-data-bundle n'imprime alors aucun JSON-LD pour elle.

use Phil\IndexingConsent\Indexing;

$directory->changeIndexing($reference, Indexing::NotIndexed, new Actor('ada'));
$directory->listing($reference)->indexing();       // Indexing::NotIndexed
$directory->listing($reference)->isSiteListed();   // true

Seuls l'auteur et les administrateurs le changent ; toute autre personne est refusée par un ListingRefused. Le choix est gardé dans deux colonnes de la fiche, sa valeur et si le site la liste, sur lesquelles filtrent les requêtes des lignes publiées : la liste d'un genre et les fiches liées gardent ce que permet Reader::SiteListings, le plan du site ce que permet Reader::Sitemap, dans la même requête unique.

Publication et historique

Chaque modification est une version, exactement comme pour tout contenu : modifier une version publiée commence un nouveau brouillon, et la version publiée reste en ligne jusqu'à ce qu'un administrateur publie la suivante. Chaque version enregistre qui l'a modifiée, dans editedBy.

Par défaut, la modification d'un auteur ou d'un rédacteur reste un brouillon ; celle d'un administrateur est publiée sur-le-champ. Avec publish_every_change, chaque modification est publiée sur-le-champ.

Directory::history() énumère les versions d'une fiche dans une locale, de la plus récente à la plus ancienne, chacune avec son numéro, son titre, la personne qui l'a modifiée et le moment de sa publication, lues en une seule requête. Directory::find() lit la fiche publiée d'un slug ; un ancien slug donne la page avec movedTo, à laquelle le contrôleur répond par une 301.

Les identifiants d'utilisateur et leur effacement

Une fiche conserve l'identifiant de son auteur et de ses rédacteurs, et chaque version l'identifiant de la personne qui l'a écrite (editedBy) : la chaîne que porte l'acteur, que Actors tire du getUserIdentifier() du jeton de sécurité. La page des rédacteurs montre l'auteur à ceux qui gèrent les rédacteurs, et l'historique montre l'auteur de chaque version à l'auteur, aux rédacteurs et aux administrateurs de la fiche. Lorsque cet identifiant est une adresse de courriel, ces personnes voient les adresses les unes des autres : une application qui ne le veut pas donne à ses utilisateurs un identifiant opaque, ou remplace les gabarits editors et history pour afficher plutôt un nom d'affichage.

Les identifiants sont conservés aussi longtemps que la fiche et ses versions, c'est-à-dire à vie : l'historique est celui de la fiche. Lorsqu'un compte est effacé, l'application efface aussi son identifiant du répertoire, comme l'exigent la Loi 25 et le RGPD :

$directory->eraseUser('ada@example.com');   // how many listings and versions changed

eraseUser() retire l'identifiant des fiches qu'il a rédigées — elles n'ont plus d'auteur, et les administrateurs nomment leurs rédacteurs — et de celles qu'il modifie, et vide le editedBy de chaque version qu'il a écrite, brouillon ou publiée, en une seule transaction (UserErasure, dont l'adaptateur Doctrine est DoctrineUserErasure). Les fiches et leurs versions restent, avec ce qu'elles disent.