From 1e77ec3447992d16ae2f810e51c4b70b540e04f9 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Fri, 24 Jul 2026 10:32:29 +0200 Subject: [PATCH] refactor(#286): rename computed genre cascade keys to resolved_* + escape in view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The book genre hierarchy exposed two request-only computed keys, genere_id_cascade and sottogenere_id_cascade, sitting right next to the stored libri columns genere_id/sottogenere_id with no signal that they are derived, not persisted — which is exactly what read as redundant/misleading in discussion #286. Rename them to resolved_genere_id / resolved_sottogenere_id so the `resolved_` prefix marks them as a derived view of the stored ids. resolveGenreHierarchy() (producer) and its two consumers — the edit form's cascade pre-population and the admin book-detail breadcrumb (#287) — are updated together; the keys are purely in-memory (never a column, never serialized to any API), so the blast radius is these three files. radice_id/radice_nome/genere_nome/sottogenere_nome are intentionally NOT renamed: they are also base-query SELECT aliases consumed by the API controllers, exports and imports. Also switched the breadcrumb label from HtmlHelper::e() to the view-standard htmlspecialchars(..., ENT_QUOTES, 'UTF-8') (the earlier CodeRabbit nit on #287). Verified live: a book stored at root+direct-child (genere_id=root, sottogenere_id=child) renders the breadcrumb as "Root -> Child" with the correct distinct ids, not the old duplicate/empty chain. php -l + PHPStan level 5 clean. The genre regression in full-test.spec.js (Phase 18) covers it. --- app/Models/BookRepository.php | 34 +++++++++++++++----------- app/Views/libri/partials/book_form.php | 4 +-- app/Views/libri/scheda_libro.php | 6 ++--- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/Models/BookRepository.php b/app/Models/BookRepository.php index efc69df66..01a22c4ab 100644 --- a/app/Models/BookRepository.php +++ b/app/Models/BookRepository.php @@ -1690,6 +1690,12 @@ public function updateOptionals(int $bookId, array $data): void * then maps IDs to the correct cascade levels so the edit form can * pre-populate all three dropdowns. * + * Adds COMPUTED, request-only keys prefixed `resolved_` — `resolved_genere_id` + * and `resolved_sottogenere_id` — that hold the depth-correct ids for the + * three dropdown slots. They are NOT columns on `libri` (which stores only + * `genere_id`/`sottogenere_id`); the `resolved_` prefix marks them as a + * derived view of the stored ids, not duplicates (see discussion #286). + * * @param array $row Book row (modified in place) */ private function resolveGenreHierarchy(array &$row): void @@ -1735,9 +1741,9 @@ private function resolveGenreHierarchy(array &$row): void $row['radice_id'] = 0; $row['radice_nome'] = null; $row['genere_nome'] = null; - $row['genere_id_cascade'] = 0; + $row['resolved_genere_id'] = 0; $row['sottogenere_nome'] = null; - $row['sottogenere_id_cascade'] = 0; + $row['resolved_sottogenere_id'] = 0; return; } @@ -1776,43 +1782,43 @@ private function resolveGenreHierarchy(array &$row): void // L2 + deepest descendant $deepest = end($subChain); $row['genere_nome'] = $subChain[0]['nome']; - $row['genere_id_cascade'] = (int)$subChain[0]['id']; + $row['resolved_genere_id'] = (int)$subChain[0]['id']; $row['sottogenere_nome'] = $deepest['nome']; - $row['sottogenere_id_cascade'] = (int)$deepest['id']; + $row['resolved_sottogenere_id'] = (int)$deepest['id']; } elseif (count($subChain) === 1) { // Direct child of root → L2 only $row['genere_nome'] = $subChain[0]['nome']; - $row['genere_id_cascade'] = (int)$subChain[0]['id']; + $row['resolved_genere_id'] = (int)$subChain[0]['id']; $row['sottogenere_nome'] = null; - $row['sottogenere_id_cascade'] = 0; + $row['resolved_sottogenere_id'] = 0; } else { $row['genere_nome'] = null; - $row['genere_id_cascade'] = 0; + $row['resolved_genere_id'] = 0; $row['sottogenere_nome'] = null; - $row['sottogenere_id_cascade'] = 0; + $row['resolved_sottogenere_id'] = 0; } } else { $row['genere_nome'] = null; - $row['genere_id_cascade'] = 0; + $row['resolved_genere_id'] = 0; $row['sottogenere_nome'] = null; - $row['sottogenere_id_cascade'] = 0; + $row['resolved_sottogenere_id'] = 0; } } elseif ($chainLen === 2) { // genere_id points to L2 genre — standard case $row['radice_id'] = $chain[0]['id']; $row['radice_nome'] = $chain[0]['nome']; $row['genere_nome'] = $chain[1]['nome']; - $row['genere_id_cascade'] = $chain[1]['id']; - $row['sottogenere_id_cascade'] = $sottogenereId; + $row['resolved_genere_id'] = $chain[1]['id']; + $row['resolved_sottogenere_id'] = $sottogenereId; } else { // genere_id points to L3+ — stored at a deeper level // Map: root=chain[0], genre=chain[1], sotto=genere_id $row['radice_id'] = $chain[0]['id']; $row['radice_nome'] = $chain[0]['nome']; $row['genere_nome'] = $chain[1]['nome']; - $row['genere_id_cascade'] = $chain[1]['id']; + $row['resolved_genere_id'] = $chain[1]['id']; $row['sottogenere_nome'] = $chain[$chainLen - 1]['nome']; - $row['sottogenere_id_cascade'] = $chain[$chainLen - 1]['id']; + $row['resolved_sottogenere_id'] = $chain[$chainLen - 1]['id']; } } diff --git a/app/Views/libri/partials/book_form.php b/app/Views/libri/partials/book_form.php index 8bd33c519..2630710ca 100644 --- a/app/Views/libri/partials/book_form.php +++ b/app/Views/libri/partials/book_form.php @@ -51,8 +51,8 @@ $initialData = [ 'id' => (int)($book['id'] ?? 0), 'radice_id' => (int)($book['radice_id'] ?? 0), - 'genere_id' => (int)(($book['genere_id_cascade'] ?? null) ?: ($book['genere_id'] ?? 0)), - 'sottogenere_id' => (int)(($book['sottogenere_id_cascade'] ?? null) ?: ($book['sottogenere_id'] ?? 0)), + 'genere_id' => (int)(($book['resolved_genere_id'] ?? null) ?: ($book['genere_id'] ?? 0)), + 'sottogenere_id' => (int)(($book['resolved_sottogenere_id'] ?? null) ?: ($book['sottogenere_id'] ?? 0)), 'classificazione_dewey' => $book['classificazione_dewey'] ?? '', 'editore_id' => (int)($book['editore_id'] ?? 0), 'editore_nome' => $book['editore_nome'] ?? '', diff --git a/app/Views/libri/scheda_libro.php b/app/Views/libri/scheda_libro.php index f8f7d8753..075368e1b 100644 --- a/app/Views/libri/scheda_libro.php +++ b/app/Views/libri/scheda_libro.php @@ -240,14 +240,14 @@ class="inline-flex items-center px-2 py-1 rounded-full text-sm bg-gray-100 text- $genreParts[] = [$id, $name]; }; $addGenrePart((int)($libro['radice_id'] ?? 0), $libro['radice_nome'] ?? null); - $addGenrePart((int)($libro['genere_id_cascade'] ?? $libro['genere_id'] ?? 0), $libro['genere_nome'] ?? null); - $addGenrePart((int)($libro['sottogenere_id_cascade'] ?? $libro['sottogenere_id'] ?? 0), $libro['sottogenere_nome'] ?? null); + $addGenrePart((int)($libro['resolved_genere_id'] ?? $libro['genere_id'] ?? 0), $libro['genere_nome'] ?? null); + $addGenrePart((int)($libro['resolved_sottogenere_id'] ?? $libro['sottogenere_id'] ?? 0), $libro['sottogenere_nome'] ?? null); ?> $gp): ?> 0): ?> + class="text-gray-900 hover:text-gray-600 hover:underline font-semibold">