Skip to content

Commit d8da04f

Browse files
committed
Fix RouteNormalizer entity mutation corrupting page/pageData DB links
RouteNormalizer called setPage/setPageData on redirect routes before normalizing them. Both setters call $page->setRoute($this) as a side effect, moving the page's owning-side FK away from its correct route. In worker mode the entity manager persists between requests, so this dirty state is eventually flushed to the DB — resulting in page: null on the chapter route and pageData: null on the parent redirect route. Fix: use ReflectionProperty to set page/pageData directly (bypassing setter side effects) before normalization, then restore original values. Only propagates from the final redirect target when the own value is null. Behat tests added: route with page+redirect returns page IRI; route with pageData+redirect returns pageData IRI.
1 parent 1809e5d commit d8da04f

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,14 @@ Services instrumented with optional `?CwaCollectorData` arg: `JWTEventListener`,
620620

621621
---
622622

623+
## ~~Bug: `RouteNormalizer` mutates page/pageData entity state during serialization~~ — FIXED
624+
625+
**Fixed (committed below):** `RouteNormalizer.normalize()` previously called `$object->setPage($finalRoute->getPage())` and `$object->setPageData($finalRoute->getPageData())` before normalizing a redirect route. Both setters call `$page->setRoute($this)` as a side effect, which corrupts Doctrine's identity map by moving the page/pageData's owning-side FK away from its correct route. In worker mode (FrankenPHP), the entity manager persists between requests so this dirty state is eventually flushed to the DB, causing `page: null` on the chapter route and `pageData: null` on the redirect route.
626+
627+
**Fix:** Use `ReflectionProperty` to set `page` / `pageData` directly on the Route (bypassing the setters) before normalizing, then restore the original values after. Only sets from the final redirect target when the route has no own value (`null` check). Behat tests added for: route with page+redirect returns page IRI; route with pageData+redirect returns pageData IRI.
628+
629+
---
630+
623631
## Known Configuration Quirks
624632

625633
*(none)*

features/bootstrap/DoctrineContext.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,70 @@ public function thereIsARouteWithAPage(string $path): void
664664
$this->restContext->resources['route_page'] = $this->iriConverter->getIriFromResource($page);
665665
}
666666

667+
/**
668+
* @Given there is a Route :path with a page and a redirect to :redirectPath
669+
*/
670+
public function thereIsARouteWithPageAndRedirect(string $path, string $redirectPath): void
671+
{
672+
$childRoute = new Route();
673+
$childRoute->setPath($redirectPath)->setName($redirectPath);
674+
$this->timestampedHelper->persistTimestampedFields($childRoute, true);
675+
$this->manager->persist($childRoute);
676+
677+
$page = new Page();
678+
$page->isTemplate = false;
679+
$page->reference = 'route-page';
680+
$this->timestampedHelper->persistTimestampedFields($page, true);
681+
$this->manager->persist($page);
682+
683+
$parentRoute = new Route();
684+
$parentRoute->setPath($path)->setName($path)->setRedirect($childRoute);
685+
$parentRoute->setPage($page);
686+
$this->timestampedHelper->persistTimestampedFields($parentRoute, true);
687+
$this->manager->persist($parentRoute);
688+
689+
$this->manager->flush();
690+
691+
$this->restContext->resources['route'] = $this->iriConverter->getIriFromResource($parentRoute);
692+
$this->restContext->resources['route_page'] = $this->iriConverter->getIriFromResource($page);
693+
$this->restContext->resources['child_route'] = $this->iriConverter->getIriFromResource($childRoute);
694+
}
695+
696+
/**
697+
* @Given there is a Route :path with a pageData and a redirect to :redirectPath
698+
*/
699+
public function thereIsARouteWithPageDataAndRedirect(string $path, string $redirectPath): void
700+
{
701+
$childRoute = new Route();
702+
$childRoute->setPath($redirectPath)->setName($redirectPath);
703+
$this->timestampedHelper->persistTimestampedFields($childRoute, true);
704+
$this->manager->persist($childRoute);
705+
706+
$page = new Page();
707+
$page->isTemplate = true;
708+
$page->reference = 'template-page';
709+
$this->timestampedHelper->persistTimestampedFields($page, true);
710+
$this->manager->persist($page);
711+
712+
$pageData = new \Silverback\ApiComponentsBundle\Tests\Functional\TestBundle\Entity\PageData();
713+
$pageData->setTitle('Test PageData');
714+
$pageData->page = $page;
715+
$this->timestampedHelper->persistTimestampedFields($pageData, true);
716+
$this->manager->persist($pageData);
717+
718+
$parentRoute = new Route();
719+
$parentRoute->setPath($path)->setName($path)->setRedirect($childRoute);
720+
$parentRoute->setPageData($pageData);
721+
$this->timestampedHelper->persistTimestampedFields($parentRoute, true);
722+
$this->manager->persist($parentRoute);
723+
724+
$this->manager->flush();
725+
726+
$this->restContext->resources['route'] = $this->iriConverter->getIriFromResource($parentRoute);
727+
$this->restContext->resources['page_data'] = $this->iriConverter->getIriFromResource($pageData);
728+
$this->restContext->resources['child_route'] = $this->iriConverter->getIriFromResource($childRoute);
729+
}
730+
667731
/**
668732
* @Given there is a Route :path which redirects to :redirectTo
669733
*/

features/main/route.feature

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ Feature: Route resources
7373
And the JSON node redirectPath should be equal to the string "/contact-new"
7474
And the JSON node page should be equal to the string "resource[route_page]"
7575

76+
Scenario: A route with its own page and a redirect still returns the page IRI
77+
Given there is a Route "/parent" with a page and a redirect to "/child"
78+
When I send a "GET" request to "/_/routes//parent"
79+
Then the response status code should be 200
80+
And the JSON node page should be equal to the string "resource[route_page]"
81+
82+
Scenario: A route with its own pageData and a redirect still returns the pageData IRI
83+
Given there is a Route "/parent" with a pageData and a redirect to "/child"
84+
When I send a "GET" request to "/_/routes//parent"
85+
Then the response status code should be 200
86+
And the JSON node pageData should be equal to the string "resource[page_data]"
87+
7688
# Route generate
7789
@loginUser
7890
Scenario: I can automatically generate a route from a PageData resource

src/Serializer/Normalizer/RouteNormalizer.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,29 @@ public function normalize($object, $format = null, array $context = []): float|a
5252
}
5353

5454
$isRedirect = $finalRoute !== $object;
55+
5556
if ($isRedirect) {
56-
$object->setPage($finalRoute->getPage());
57-
$object->setPageData($finalRoute->getPageData());
57+
// Use reflection to temporarily propagate page/pageData from the final route for serialization.
58+
// We must NOT call setPage/setPageData: those setters call $page->setRoute($this),
59+
// which corrupts Doctrine's identity map and causes stale data to be flushed to the DB.
60+
$reflPage = new \ReflectionProperty($object, 'page');
61+
$reflPageData = new \ReflectionProperty($object, 'pageData');
62+
$originalPage = $reflPage->getValue($object);
63+
$originalPageData = $reflPageData->getValue($object);
64+
if (null === $originalPage) {
65+
$reflPage->setValue($object, $finalRoute->getPage());
66+
}
67+
if (null === $originalPageData) {
68+
$reflPageData->setValue($object, $finalRoute->getPageData());
69+
}
5870
}
5971

6072
$normalized = $this->normalizer->normalize($object, $format, $context);
6173

6274
if ($isRedirect) {
6375
$normalized['redirectPath'] = $finalRoute->getPath();
76+
$reflPage->setValue($object, $originalPage);
77+
$reflPageData->setValue($object, $originalPageData);
6478
}
6579

6680
return $normalized;

0 commit comments

Comments
 (0)