Skip to content

Commit 1c54827

Browse files
jbidadclaude
andcommitted
Store the route cache as a serialized payload
Replace the var_export()ed PHP array in the route cache file with a serialized payload that is rehydrated via unserialize(). The cache file remains a require()-able PHP file, so previously generated caches and any tooling that loads it directly keep working. The route service provider now loads serialized payloads natively through Router::setCompiledRoutes() and falls back to require() for legacy PHP cache files, keeping full backward compatibility for applications that upgrade without re-running route:cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dcf70c4 commit 1c54827

5 files changed

Lines changed: 156 additions & 9 deletions

File tree

src/Illuminate/Foundation/Console/RouteCacheCommand.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ protected function getFreshApplication()
103103
*/
104104
protected function buildRouteCacheFile(RouteCollection $routes)
105105
{
106-
$stub = $this->files->get(__DIR__.'/stubs/routes.stub');
107-
108-
return str_replace('{{routes}}', var_export($routes->compile(), true), $stub);
106+
return sprintf(
107+
"<?php\n\napp('router')->setCompiledRoutes(\n unserialize(%s)\n);\n",
108+
var_export(serialize($routes->compile()), true)
109+
);
109110
}
110111
}

src/Illuminate/Foundation/Console/stubs/routes.stub

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,20 @@ protected function loadCachedRoutes()
147147
}
148148

149149
$this->app->booted(function () {
150-
require $this->app->getCachedRoutesPath();
150+
$path = $this->app->getCachedRoutesPath();
151+
152+
$routes = @unserialize(file_get_contents($path));
153+
154+
if (is_array($routes)) {
155+
$this->app['router']->setCompiledRoutes($routes);
156+
157+
return;
158+
}
159+
160+
// Backward compatibility: route cache files generated by previous
161+
// versions of the framework are PHP files that register the
162+
// compiled routes themselves, so we require them as before.
163+
require $path;
151164
});
152165
}
153166

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Routing;
4+
5+
use Illuminate\Container\Container;
6+
use Illuminate\Events\Dispatcher;
7+
use Illuminate\Foundation\Support\Providers\RouteServiceProvider;
8+
use Illuminate\Routing\Router;
9+
use Orchestra\Testbench\TestCase;
10+
11+
class RouteCacheLoadingTest extends TestCase
12+
{
13+
/**
14+
* The path to the temporary route cache file used by the current test.
15+
*/
16+
protected ?string $cachePath = null;
17+
18+
/** {@inheritDoc} */
19+
#[\Override]
20+
protected function getPackageProviders($app)
21+
{
22+
return [RouteServiceProvider::class];
23+
}
24+
25+
/** {@inheritDoc} */
26+
#[\Override]
27+
protected function tearDown(): void
28+
{
29+
if ($this->cachePath !== null && is_file($this->cachePath)) {
30+
@unlink($this->cachePath);
31+
}
32+
33+
unset($_ENV['APP_ROUTES_CACHE'], $_SERVER['APP_ROUTES_CACHE']);
34+
35+
parent::tearDown();
36+
}
37+
38+
public function testItLoadsRoutesFromASerializedPayloadCacheFile()
39+
{
40+
// This is the format produced by the "route:cache" command: a require()-able
41+
// PHP file whose route data is stored as a serialized string.
42+
$this->bootWithCachedRoutes(sprintf(
43+
"<?php\n\napp('router')->setCompiledRoutes(\n unserialize(%s)\n);\n",
44+
var_export(serialize($this->compiledRoutes()), true)
45+
));
46+
47+
$this->assertTrue($this->app->routesAreCached());
48+
49+
$this->get('cached-route')->assertOk()->assertSee('cached response');
50+
}
51+
52+
public function testItLoadsRoutesFromAPlainSerializedCacheFile()
53+
{
54+
// A cache file that contains only serialized route data (no PHP wrapper) is
55+
// read with file_get_contents()/unserialize() and handed to the router via
56+
// setCompiledRoutes().
57+
$this->bootWithCachedRoutes(serialize($this->compiledRoutes()));
58+
59+
$this->assertTrue($this->app->routesAreCached());
60+
61+
$this->get('cached-route')->assertOk()->assertSee('cached response');
62+
}
63+
64+
public function testItStillLoadsLegacyVarExportCacheFiles()
65+
{
66+
// Route cache files generated by previous versions of the framework dumped
67+
// the compiled routes as a var_export()ed PHP array. They must keep working
68+
// for applications that upgrade without re-running "route:cache".
69+
$this->bootWithCachedRoutes(sprintf(
70+
"<?php\n\napp('router')->setCompiledRoutes(\n %s\n);\n",
71+
var_export($this->compiledRoutes(), true)
72+
));
73+
74+
$this->assertTrue($this->app->routesAreCached());
75+
76+
$this->get('cached-route')->assertOk()->assertSee('cached response');
77+
}
78+
79+
/**
80+
* Write the given contents to a temporary route cache file and reboot the
81+
* application so the route service provider loads it.
82+
*/
83+
protected function bootWithCachedRoutes(string $contents): void
84+
{
85+
$this->cachePath = tempnam(sys_get_temp_dir(), 'route-cache-test-');
86+
87+
file_put_contents($this->cachePath, $contents);
88+
89+
$_ENV['APP_ROUTES_CACHE'] = $_SERVER['APP_ROUTES_CACHE'] = $this->cachePath;
90+
91+
$this->reloadApplication();
92+
}
93+
94+
/**
95+
* Build a compiled route cache array containing a single named route.
96+
*
97+
* @return array{compiled: array, attributes: array}
98+
*/
99+
protected function compiledRoutes(): array
100+
{
101+
$router = new Router(new Dispatcher, new Container);
102+
103+
$router->get('cached-route', ['uses' => RouteCacheLoadingController::class.'@index'])->name('cached-route');
104+
105+
$router->getRoutes()->refreshNameLookups();
106+
$router->getRoutes()->refreshActionLookups();
107+
108+
return $router->getRoutes()->compile();
109+
}
110+
}
111+
112+
class RouteCacheLoadingController
113+
{
114+
public function index()
115+
{
116+
return 'cached response';
117+
}
118+
}

tests/Integration/Routing/RouteCachingTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ public function testRedirectRoutes()
2323
$this->get('/foo/1')->assertRedirect('/foo/1/bar');
2424
}
2525

26+
public function testCachedRouteFileStoresASerializedPayload()
27+
{
28+
$this->routes(__DIR__.'/Fixtures/wildcard_catch_all_routes.php');
29+
30+
$contents = file_get_contents($this->app->getCachedRoutesPath());
31+
32+
// The route cache file remains a require()-able PHP file so that it stays
33+
// compatible with previously generated caches and any tooling that loads
34+
// it directly...
35+
$this->assertStringContainsString("app('router')->setCompiledRoutes(", $contents);
36+
37+
// ...but the compiled route data is now stored as a serialized string that
38+
// is rehydrated with unserialize() instead of being dumped into the file as
39+
// a large var_export()ed PHP array.
40+
$this->assertStringContainsString('unserialize(', $contents);
41+
42+
// The cached routes still resolve correctly once the file is loaded.
43+
$this->get('/foo')->assertSee('Regular route');
44+
}
45+
2646
protected function routes(string $file)
2747
{
2848
$this->defineCacheRoutes(file_get_contents($file));

0 commit comments

Comments
 (0)