From 88c31d709b6cefb8e15468a976110806534d3a96 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 1 Apr 2026 22:49:13 -0400 Subject: [PATCH 1/3] Serialize nocache regions before storing in cache Wrap Region objects in serialize() before putting them in the cache store so the cache layer only sees a string. This avoids issues with Laravel 13's serializable_classes allowlist, since Region context can contain arbitrary objects. We control the unserialize() call ourselves with allowed_classes => true. Co-Authored-By: Claude Opus 4.6 --- src/StaticCaching/NoCache/DatabaseSession.php | 2 +- src/StaticCaching/NoCache/Session.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticCaching/NoCache/DatabaseSession.php b/src/StaticCaching/NoCache/DatabaseSession.php index c5b14b76851..9e32d8caa95 100644 --- a/src/StaticCaching/NoCache/DatabaseSession.php +++ b/src/StaticCaching/NoCache/DatabaseSession.php @@ -30,7 +30,7 @@ public function region(string $key): Region throw new RegionNotFound($key); } - return unserialize($region->region); + return unserialize($region->region, ['allowed_classes' => true]); } protected function cacheRegion(Region $region) diff --git a/src/StaticCaching/NoCache/Session.php b/src/StaticCaching/NoCache/Session.php index 1b6bce0f3e0..dc3478a7af3 100644 --- a/src/StaticCaching/NoCache/Session.php +++ b/src/StaticCaching/NoCache/Session.php @@ -51,7 +51,7 @@ public function regions(): Collection public function region(string $key): Region { if ($this->regions->contains($key) && ($region = StaticCache::cacheStore()->get('nocache::region.'.$key))) { - return $region; + return $region instanceof Region ? $region : unserialize($region, ['allowed_classes' => true]); } throw new RegionNotFound($key); @@ -150,6 +150,6 @@ protected function resolvePageAndPathForPagination(): void protected function cacheRegion(Region $region) { - StaticCache::cacheStore()->forever('nocache::region.'.$region->key(), $region); + StaticCache::cacheStore()->forever('nocache::region.'.$region->key(), serialize($region)); } } From 7b920edae64743d22363718940e35e3be1bbbc71 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 2 Apr 2026 09:18:01 -0400 Subject: [PATCH 2/3] Add test for nocache region serialization round-trip Co-Authored-By: Claude Opus 4.6 --- tests/StaticCaching/NoCacheSessionTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/StaticCaching/NoCacheSessionTest.php b/tests/StaticCaching/NoCacheSessionTest.php index 507825fa8a2..14f12a79a33 100644 --- a/tests/StaticCaching/NoCacheSessionTest.php +++ b/tests/StaticCaching/NoCacheSessionTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Mockery; use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\StaticCache; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\NoCache\Session; use Statamic\StaticCaching\NoCache\StringRegion; @@ -148,6 +149,23 @@ public function it_restores_from_cache() $this->assertEquals('http://localhost/cp', $cascade['cp_url']); } + #[Test] + public function it_serializes_and_unserializes_regions_through_cache() + { + $session = new Session('http://localhost/test'); + + $region = $session->pushRegion('the contents', ['foo' => 'bar'], '.html'); + + $cached = StaticCache::cacheStore()->get('nocache::region.'.$region->key()); + $this->assertIsString($cached, 'Region should be stored as a serialized string, not an object.'); + + $retrieved = $session->region($region->key()); + + $this->assertInstanceOf(StringRegion::class, $retrieved); + $this->assertEquals($region->key(), $retrieved->key()); + $this->assertEquals(['foo' => 'bar'], $retrieved->context()); + } + #[Test] public function a_singleton_is_bound_in_the_container() { From fd4bcff3c5b924c65083120d159ff5c34732b040 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Thu, 2 Apr 2026 09:28:06 -0400 Subject: [PATCH 3/3] Handle __PHP_Incomplete_Class in cached nocache regions Co-Authored-By: Claude Opus 4.6 --- src/StaticCaching/NoCache/Session.php | 8 +++++++- tests/StaticCaching/NoCacheSessionTest.php | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/StaticCaching/NoCache/Session.php b/src/StaticCaching/NoCache/Session.php index dc3478a7af3..2935e163d37 100644 --- a/src/StaticCaching/NoCache/Session.php +++ b/src/StaticCaching/NoCache/Session.php @@ -51,7 +51,13 @@ public function regions(): Collection public function region(string $key): Region { if ($this->regions->contains($key) && ($region = StaticCache::cacheStore()->get('nocache::region.'.$key))) { - return $region instanceof Region ? $region : unserialize($region, ['allowed_classes' => true]); + if ($region instanceof Region) { + return $region; + } + + if (is_string($region)) { + return unserialize($region, ['allowed_classes' => true]); + } } throw new RegionNotFound($key); diff --git a/tests/StaticCaching/NoCacheSessionTest.php b/tests/StaticCaching/NoCacheSessionTest.php index 14f12a79a33..a0b755c27c4 100644 --- a/tests/StaticCaching/NoCacheSessionTest.php +++ b/tests/StaticCaching/NoCacheSessionTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\StaticCache; use Statamic\StaticCaching\Cacher; +use Statamic\StaticCaching\NoCache\RegionNotFound; use Statamic\StaticCaching\NoCache\Session; use Statamic\StaticCaching\NoCache\StringRegion; use Tests\FakesContent; @@ -166,6 +167,22 @@ public function it_serializes_and_unserializes_regions_through_cache() $this->assertEquals(['foo' => 'bar'], $retrieved->context()); } + #[Test] + public function it_throws_region_not_found_when_cached_region_is_an_incomplete_class() + { + $session = new Session('http://localhost/test'); + + $region = $session->pushRegion('the contents', ['foo' => 'bar'], '.html'); + + // Simulate what happens when serializable_classes enforcement + // turns a cached Region object into __PHP_Incomplete_Class. + StaticCache::cacheStore()->forever('nocache::region.'.$region->key(), new \__PHP_Incomplete_Class); + + $this->expectException(RegionNotFound::class); + + $session->region($region->key()); + } + #[Test] public function a_singleton_is_bound_in_the_container() {