Skip to content

MVP 2 — Complex properties & fine-grained customization

Property-based testing harness (BlackBox) Proposed
Existing.Example-driven tests only (integration fixtures + E2E declarative assertions). The BlackBox references (innmind.org/BlackBox) are recorded in @contexts/e2e.md as the future property-based testing basis.
Expected.BlackBox wired as the property-based testing harness: deterministic runner (fixed seed), shrinking of failing cases, integrated into the Makefile and CI — probing the Collect & Computed pipeline (facts → formatter → render) and the CRUD write path with a rich generated dataset.
Prerequisites.MVP 1 — CRUD lane operational (forms, delete): the harness varies data over the stabilized write + read paths, which requires the facts carried by PropertyMetadata (the Collect & Computed foundation) and the WidgetResolver (form mapping).
Analysis.Opens MVP 2 deliberately: once the CRUD is operational is exactly when a rich data game puts the bundle to the test — invariants over the Collect & Computed pipeline (facts → deductions, render never throws, values round-trip) that example-driven tests cannot probe exhaustively. Deterministic by design (fixed seed), shrinks failures (BlackBox), and lands in CI + local make per the deterministic-tooling principle — never a one-shot check. The E2E assertion patterns already built are its base.
Complex properties Proposed
Existing.Index/show already cover simple scalars and read-only associations; an embedded-fields metadata base is in place.
Expected.Arrays, objects and all Doctrine association types handled in forms and display; embedded fields deepened.
Prerequisites.MVP 1 — CRUD lane (type detection rework, WidgetResolver form mapping, forms, delete action).
Fine-grained form customization Proposed
Expected.Per-property form customization (widget, constraints, labels), configurable like the formatters.
Prerequisites.MVP 1 — Collect & Computed foundation and WidgetResolver (facts carried by PropertyMetadata).
Richer formatters Proposed
Expected.Four small stories: address-to-map, color picker, calendar, multi-select.
Prerequisites.MVP 1 — Minimal design decision: they require JS, blocked by the design lane.
JSON output Proposed
Existing.HTML via the twig responders; the headless JSON rendering is already an interface in germ.
Expected.JSON endpoints — scope (endpoints, shape, media-type) and timing to decide.
Prerequisites.MVP 1 — CRUD lane.
Navigation between related entities Proposed
Expected.Navigate from one entity to its related ones (UX open: breadcrumbs? linked pages?).
Prerequisites.MVP 1 — CRUD lane.
In-admin documentation Proposed
Expected.Developer help embedded in the admin pages.
Prerequisites.Coupled to the design lane — to decide.
Pagination Proposed
Existing.Index::__invoke() appelle $repository->findAll() — toutes les entités sont chargées en mémoire, sans limite ni offset. Pas de paramètre de page dans l'URL, pas de contrôle de la taille de page.
Expected.La page index affiche les entités par pages. La taille de page est configurable par entité (config karross) avec une valeur par défaut raisonnable (25). L'état de la page courante est dans l'URL (?page=2 ou /page/2) — bookmarkable, partageable. Le repository utilise Query::setMaxResults()/setFirstResult() au lieu de findAll(). Les liens previous/next sont rendus dans le template.
Prerequisites.None.
Plan.
  1. Config : ajouter un nœud entities.{FQCN}.page_size (int, défaut 25) dans Configuration.php.
  2. Index action : extraire le paramètre page de la requête (défaut 1), calculer l'offset (($page - 1) * $pageSize), utiliser un Query avec setMaxResults($pageSize) et setFirstResult($offset) au lieu de findAll(). Compter le total (COUNT) pour savoir s'il y a une page suivante.
  3. Route : ajouter un paramètre optionnel page dans le pattern de route index (ou le garder en query string — REC decision).
  4. Template : ajouter un partial templates/index/pagination.html.twig avec liens previous/next, numéro de page courant, rendu conditionnel (pas de pagination si une seule page).
  5. Tests : test d'intégration vérifiant le comportement pagination (page 1 avec 25 résultats, page 2 avec le reste). Test E2E vérifiant la navigation entre pages.
Property visibility & ordering Proposed
Existing.Toutes les propriétés Doctrine (champs + associations) sont toujours affichées, dans l'ordre de ClassMetadata::getFieldNames() puis getAssociationNames(). Pas de mécanisme pour masquer une propriété, en afficher certaines uniquement en index ou en show, ou changer l'ordre des colonnes. Les templates itèrent entityMetadata.getProperties() sans filtre.
Expected.Par entité, contrôle des propriétés affichées en index et en show, et de leur ordre. Le défaut raisonnable reste « tout afficher dans l'ordre Doctrine » — l'override est optionnel. La config porte un tableau ordonné de noms de propriétés ; seules les propriétés listées sont rendues, dans l'ordre donné. Un écran « password » ou « hashedToken » peut être masqué de l'index tout en restant présent en show.
Prerequisites.None.
Plan.
  1. Config : ajouter entities.{FQCN}.index_properties (string array, nullable) et entities.{FQCN}.show_properties (string array, nullable) dans Configuration.php. null = toutes les propriétés (comportement actuel).
  2. EntityMetadata : ajouter deux propriétés readonly array $indexProperties et readonly array $showProperties (listes de noms de propriétés, ou vide = toutes). Les filtrer depuis ComputedMetadataBuilder en fonction de la config.
  3. Templates index/show : itérer entityMetadata.getIndexProperties() (ou getShowProperties()) au lieu de getProperties() pour les en-têtes et les cellules. Quand la liste est vide, fallback sur getProperties().
  4. Tests : test d'intégration vérifiant qu'avec une config index_properties: ['title', 'published'], seules ces colonnes apparaissent dans le HTML rendu.
Database sort Proposed
Existing.Aucun tri — les entités sont rendues dans l'ordre de findAll() (ordre d'insertion/ID par défaut Doctrine). Pas de liens de tri dans les en-têtes de colonne, pas de paramètre de tri dans l'URL.
Expected.Tri par colonne en index, au niveau DB via Doctrine QueryBuilder (pas en mémoire). L'état du tri est dans l'URL (query string : ?sort=title&direction=asc) — bookmarkable. Les en-têtes de colonne sont des liens cliquables qui basculent asc/desc. Tri sur les champs scalaires ; les associations et les colonnes composées restent non triables par défaut.
Prerequisites.Pagination (le tri est architecturalement coupled au paginated query).
Plan.
  1. Config : optionnel — entities.{FQCN}.sortable (string array, nullable) pour restreindre les colonnes triables. Défaut = tous les champs scalaires.
  2. Index action : extraire les paramètres sort et direction de la requête, valider le sort contre les propriétés triables, appliquer Query::orderBy() dans le QueryBuilder. Utiliser les paramètres liés (setParameter()) si le tri est sur une colonne Doctrine.
  3. Templates : dans items.html.twig, rendre les en-têtes de colonne scalaires comme des liens ?sort={name}&direction={asc|desc} avec une flèche d'indication. Les en-têtes d'association ne sont pas cliquables.
  4. Tests : test d'intégration vérifiant que ?sort=title&direction=asc retourne les entités triées par titre. Test E2E vérifiant le clic sur un en-tête de colonne.
Per-property filters Proposed
Existing.Aucun filtre — l'index affiche toutes les entités sans mechanisme de sélection. Pas de FilterResolver, pas de chaîne de résolution pour les filtres comme il en existe pour les formatters et les templates.
Expected.Filtres par propriété en index, avec un FilterResolver en 3e chaîne parallèle (après FormatterResolver + PropertyTemplateResolver). Chaque type Doctrine sait produire un filtre : boolean → select Oui/Non/Tous ; string → texte ; integer/decimal → plage ; datetime → plage de dates ; enum → select des cases. L'état des filtres est dans l'URL (query string) — bookmarkable. Les filtres sont combinés avec AND. La config peut restreindre les propriétés filtrables par entité.
Prerequisites.Pagination + Collect & Computed (les faits portés par PropertyMetadata pilotent la résolution du filtre).
Plan.
  1. FilterResolverInterface : créer src/Filters/Resolvers/FilterResolverInterface avec accept(?string $phpType, ?FieldMapping $fieldMapping): bool et resolve(...): FilterInterface. Le FilterInterface expose buildQuery(QueryBuilder $qb, string $alias, string $property, $value): void et renderForm(PropertyMetadata $property): string (le HTML du champ de filtre).
  2. Filtres par type : BooleanFilterResolver (select Oui/Non/Tous), StringFilterResolver (LIKE), IntegerFilterResolver (plage min/max), DecimalFilterResolver (plage), DateTimeFilterResolver (plage de dates), EnumFilterResolver (select des cases). Chaque resolver est un service taggé karross.filter.resolver.
  3. Config : entities.{FQCN}.filterable (string array, nullable) pour restreindre les propriétés filtrables. Défaut = toutes les propriétés scalaires.
  4. Index action : extraire les paramètres de filtre de la requête, les combiner avec AND dans le QueryBuilder via les filtres résolus.
  5. Template : ajouter un partial templates/index/filters.html.twig au-dessus du tableau, rendant le formulaire de filtres. Le formulaire soumet en GET avec les paramètres de filtre dans la query string.
  6. Tests : test d'intégration vérifiant qu'un filtre ?published=1 retourne uniquement les entités publiées. Test E2E vérifiant l'interaction filtre.
Reference documentation — update Proposed
Existing.The reference page created in MVP 1 covers the base configuration surface (routes, formatters, type overrides, template overrides).
Expected.Extend the reference with the MVP-2 surface: per-property form customization, widget renderers, richer formatters, JSON output, pagination, visibility, sort, filters.
Prerequisites.This MVP's Fine-grained form customization + Richer formatters: the reference documents the config surface once it stabilizes.