You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+13-3Lines changed: 13 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -171,6 +171,15 @@ Consumers:
171
171
3. Resolves name/path conflicts with a numeric suffix
172
172
4. Creates or updates the `Route` entity and calls `setRoute()` on the `PageData`
173
173
174
+
**Generation refuses when the page has a parent but the parent has no route (#245).** If `parentPage` or `parentPageData` is set and `getParentPageRoute()` returns null, `create()` throws `UnroutedParentException` before touching anything. It used to skip the prefix silently and hand the child a bare top-level path — frequently the parent's own natural path (a child at `/2027` under an unrouted conference whose natural path is `/2027`). The parent could then never take its route: `UniqueEntity('path')` rejected it with a 422 on a later, unrelated write, naming a path the user never created. The reason is **path squatting, not rendering** — rendering depth comes from the manifest either way (see **Route path concatenation — recommended, not required**). The generator must not claim a path that is not the page's to claim, so it fails where the cause is.
175
+
176
+
-`POST /_/routes/generate` returns **422** (`application/problem+json`, the message in `detail`), via `exception_to_status` in `prependApiPlatformConfig()`. 422 matches the endpoint's existing refusals (page/pageData both or neither), which are 422 validation violations. No Route is created.
177
+
-**Pages with no parent are unaffected** and still get a top-level path.
178
+
-**Explicit creation is unaffected.**`POST /_/routes` with a path, `DoctrineContext` and `CwaFixtureBuilder`'s `route:` argument never call the generator, so a routed child under an unrouted parent remains possible and remains live (the #224 guard). Refusing to *generate* a path is not refusing the page a route.
179
+
-`CwaFixtureBuilder` propagates the exception out of `flush()`: a page nested under a parent that gets no route (an `isTemplate: true` page with no `route:`) must be given an explicit `route:`.
180
+
181
+
Tests: `tests/Helper/Route/RouteGeneratorTest.php`; `features/main/route.feature` (refused with 422 and zero Routes, prefixed under a routed parent, explicit creation still 201).
182
+
174
183
### Caching architecture
175
184
176
185
Resources are designed as **individual, piecemeal, independently cacheable entities**. The API does not bundle data into large grouped responses. Each resource (Route, Page, Layout, ComponentGroup, Component, etc.) is fetched and cached separately. When a resource changes, only that resource's cache entry is invalidated — not anything that merely references it.
@@ -253,7 +262,7 @@ Pages support sub-pages. A conference page at `/best-conference-ever` renders a
253
262
254
263
A validation constraint (`Assert\Expression`) ensures both cannot be set simultaneously.
255
264
256
-
`getParentPageRoute(): ?Route` is a computed helper (no DB column) returning `$parentPage?->getRoute() ?? $parentPageData?->getRoute()`. Used by `RouteGenerator` to prefix paths. Returns null gracefully when the parent is still in draft (no public Route yet).
265
+
`getParentPageRoute(): ?Route` is a computed helper (no DB column) returning `$parentPage?->getRoute() ?? $parentPageData?->getRoute()`. Used by `RouteGenerator` to prefix paths. Returns null when the parent is still in draft (no public Route yet), in which case `RouteGenerator` refuses to generate a route for the child (#245).
257
266
258
267
### How the manifest carries parent resources
259
268
@@ -302,7 +311,7 @@ This means:
302
311
303
312
-**No `$nested` boolean** — parent = nested, full stop. The presence of `$parentPage`/`$parentPageData` is the complete signal.
304
313
-**Two FK properties, not one** — `AbstractPage` is a mapped superclass with no discriminator map; `?AbstractPage` cannot be a Doctrine FK target. `?Page` + `?AbstractPageData` mirrors `Route.$page`/`Route.$pageData`.
305
-
-**`getParentPageRoute()` is computed** — no DB column; used by `RouteGenerator` only; returns null safely when the parent has no route yet.
314
+
-**`getParentPageRoute()` is computed** — no DB column; used by `RouteGenerator` only; returns null when the parent has no route yet, and `RouteGenerator` then refuses to generate (#245).
306
315
-**Route concatenation is recommended, not required** — `RouteGenerator` prefixes child paths for clean URLs and SEO, but the module's `<CwaPage />` renders depth from manifest data, not URL structure.
307
316
-**`resource_iris` is `string[][]`, not `string[]`** — depth-grouped, root first. The module reads the array index as the rendering depth without any client-side traversal.
308
317
-**Single rendering mechanism** — `<CwaPage />` uses a manifest in both public and admin/draft contexts. Both contexts use the same `/_/resource_manifest/{id}` endpoint — route path for public, UUID for admin/draft. The chain walk (`parentPage`/`parentPageData`) is a fallback only. No URL-depth dependency.
@@ -492,6 +501,7 @@ GroupBuilder
492
501
| no `route:` on `->page()` + `isTemplate: true`| no Route created |
493
502
| no `route:` on `->page()` without template flag | RouteGenerator called from title (slug) |
494
503
|`->pageData(...)` inside `->nested()`, no route | RouteGenerator called → `/parent-path/slug-from-title`|
504
+
|`->page(...)`/`->pageData(...)` inside `->nested()` of a parent that gets no route (e.g. `isTemplate: true`), no route |`UnroutedParentException` from `flush()` — pass an explicit `route:` (#245) |
495
505
|`->pageData(...)` or `->page(...)` at top level, no route, no title | no Route created (draft) |
496
506
497
507
### Allowed components on groups
@@ -564,7 +574,7 @@ $topicBuilder->onRoutesCreated(function (array $childBuilders) use ($intro) {
564
574
565
575
-**No `$nested` boolean** — parent = nested, full stop. The presence of `$parentPage`/`$parentPageData` is the complete signal.
566
576
-**Two FK properties, not one** — `AbstractPage` is a mapped superclass with no discriminator map; `?AbstractPage` cannot be a Doctrine FK target. `?Page` + `?AbstractPageData` mirrors `Route.$page`/`Route.$pageData`.
567
-
-**`getParentPageRoute()` is computed** — no DB column; used by `RouteGenerator` only; returns null safely when the parent has no route yet.
577
+
-**`getParentPageRoute()` is computed** — no DB column; used by `RouteGenerator` only; returns null when the parent has no route yet, and `RouteGenerator` then refuses to generate (#245).
568
578
-**Route concatenation is recommended, not required** — `RouteGenerator` prefixes child paths for clean URLs and SEO, but the module's `<CwaPage />` renders depth from manifest data, not URL structure.
569
579
-**`resource_iris` is `string[][]`, not `string[]`** — depth-grouped, root first. The module reads the array index as the rendering depth without any client-side traversal.
570
580
-**Single rendering mechanism** — `<CwaPage />` uses a manifest in both public and admin/draft contexts. Both contexts use the same `/_/resource_manifest/{id}` endpoint — route path for public, UUID for admin/draft. The chain walk (`parentPage`/`parentPageData`) is a fallback only. No URL-depth dependency.
thrownewUnroutedParentException('Cannot generate a route for this page because its parent page has no route. Give the parent page a route first, or create this page\'s route explicitly.');
0 commit comments