Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions src/GenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Wayfinder;

use Illuminate\Console\Command;
use Illuminate\Contracts\Http\Kernel as HttpKernel;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Routing\Route as BaseRoute;
use Illuminate\Routing\Router;
Expand All @@ -26,6 +27,8 @@ class GenerateCommand extends Command

private $urlDefaults = [];

private $globalMiddleware = [];

private $pathDirectory = 'actions';

private $content = [];
Expand All @@ -52,21 +55,17 @@ public function handle()
$this->view->addNamespace('wayfinder', __DIR__.'/../resources');
$this->view->addExtension('blade.ts', 'blade');

$this->syncMiddlewareFromHttpKernel();

$this->forcedScheme = (new ReflectionProperty($this->url, 'forceScheme'))->getValue($this->url);
$this->forcedRoot = (new ReflectionProperty($this->url, 'forcedRoot'))->getValue($this->url);

$globalUrlDefaults = collect(URL::getDefaultParameters())->map(fn ($v) => is_scalar($v) || is_null($v) ? $v : '');
$globalUrlDefaults = collect(URL::getDefaultParameters())
->map(fn ($v) => is_scalar($v) || is_null($v) ? $v : '')
->merge($this->urlDefaultsForMiddleware($this->globalMiddleware));

$routes = collect($this->router->getRoutes())->map(function (BaseRoute $route) use ($globalUrlDefaults) {
$defaults = collect($this->router->gatherRouteMiddleware($route))->map(function ($middleware) {
if ($middleware instanceof \Closure) {
return [];
}

$this->urlDefaults[$middleware] ??= $this->getDefaultsForMiddleware($middleware);

return $this->urlDefaults[$middleware];
})->flatMap(fn ($r) => $r);
$defaults = $this->urlDefaultsForMiddleware($this->router->gatherRouteMiddleware($route));

return new Route($route, $globalUrlDefaults->merge($defaults), $this->forcedScheme, $this->forcedRoot);
});
Expand Down Expand Up @@ -98,6 +97,45 @@ public function handle()
}
}

private function syncMiddlewareFromHttpKernel(): void
{
if (! $this->laravel->bound(HttpKernel::class)) {
return;
}

$groups = $this->router->getMiddlewareGroups();
$aliases = $this->router->getMiddleware();

// Resolving the kernel syncs its middleware onto the router, overwriting existing groups
$kernel = $this->laravel->make(HttpKernel::class);

foreach ($groups as $group => $middleware) {
foreach ($middleware as $name) {
$this->router->pushMiddlewareToGroup($group, $name);
}
}

foreach ($aliases as $name => $class) {
$this->router->aliasMiddleware($name, $class);
}

// Global middleware is never synced to the router, and the getter is not on the kernel contract
if (method_exists($kernel, 'getGlobalMiddleware')) {
$this->globalMiddleware = $kernel->getGlobalMiddleware();
}
}

private function urlDefaultsForMiddleware(array $middleware): Collection
{
return collect($middleware)
->reject(fn ($name) => $name instanceof \Closure)
->flatMap(function ($name) {
$this->urlDefaults[$name] ??= $this->getDefaultsForMiddleware($name);

return $this->urlDefaults[$name];
});
}

private function writeWayfinderHelperFile(): void
{
$previousPathDirectory = $this->pathDirectory;
Expand Down
123 changes: 123 additions & 0 deletions tests/Feature/MiddlewareUrlDefaultsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Tests\Feature;

use App\Http\Middleware\GlobalUrlDefaultsMiddleware;
use App\Http\Middleware\UrlDefaultsMiddleware;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Route;
use Laravel\Wayfinder\WayfinderServiceProvider;
use Orchestra\Testbench\TestCase;

use function Illuminate\Filesystem\join_paths;

class MiddlewareUrlDefaultsTest extends TestCase
{
private string $tempPath;

private Filesystem $files;

protected function getPackageProviders($app): array
{
return [WayfinderServiceProvider::class];
}

protected function setUp(): void
{
parent::setUp();

$this->files = new Filesystem;
$this->tempPath = join_paths(sys_get_temp_dir(), 'wayfinder-middleware-'.uniqid());
}

protected function tearDown(): void
{
$this->files->deleteDirectory($this->tempPath);

parent::tearDown();
}

private function generate(string $directory): string
{
$this->artisan('wayfinder:generate', [
'--path' => $this->tempPath,
'--skip-actions' => true,
])->assertSuccessful();

return $this->files->get(join_paths($this->tempPath, 'routes', $directory, 'index.ts'));
}

public function test_url_defaults_are_resolved_from_an_aliased_middleware(): void
{
// Registering via afterResolving mirrors withMiddleware(): the alias only reaches
// the router when the HTTP kernel resolves, which is what console never does.
$this->app->afterResolving(Kernel::class, fn ($kernel) => $kernel->setMiddlewareAliases([
'url-defaults' => UrlDefaultsMiddleware::class,
]));

Route::middleware('url-defaults')->get('/alias-defaults/{locale}', fn () => '')->name('alias.defaults');

$this->assertStringContainsString("url: '/alias-defaults/{locale?}'", $this->generate('alias'));
}

public function test_url_defaults_are_resolved_from_a_kernel_middleware_group(): void
{
$this->app->afterResolving(Kernel::class, fn ($kernel) => $kernel->setMiddlewareGroups([
'tenant' => [UrlDefaultsMiddleware::class],
]));

Route::middleware('tenant')->get('/kernel-group-defaults/{locale}', fn () => '')->name('kernel.defaults');

$this->assertStringContainsString("url: '/kernel-group-defaults/{locale?}'", $this->generate('kernel'));
}

public function test_url_defaults_are_resolved_from_global_middleware(): void
{
// Global middleware never reaches the router, so this only works if the defaults
// are read off the kernel itself.
$this->app->afterResolving(Kernel::class, fn ($kernel) => $kernel->setGlobalMiddleware([
UrlDefaultsMiddleware::class,
]));

Route::get('/global-defaults/{locale}', fn () => '')->name('global.defaults');

$this->assertStringContainsString("url: '/global-defaults/{locale?}'", $this->generate('global'));
}

public function test_url_defaults_are_resolved_from_a_middleware_pushed_onto_a_group(): void
{
Route::pushMiddlewareToGroup('web', UrlDefaultsMiddleware::class);

Route::middleware('web')->get('/group-defaults/{locale}', fn () => '')->name('group.defaults');

$this->assertStringContainsString("url: '/group-defaults/{locale?}'", $this->generate('group'));
}

public function test_url_defaults_are_dropped_when_the_middleware_is_excluded_by_alias(): void
{
// Excluded middleware runs through the same alias map, so without the kernel's aliases
// the exclusion is missed and the parameter is wrongly reported as optional.
$this->app->afterResolving(Kernel::class, fn ($kernel) => $kernel->setMiddlewareAliases([
'url-defaults' => UrlDefaultsMiddleware::class,
]));

Route::middleware(UrlDefaultsMiddleware::class)
->withoutMiddleware('url-defaults')
->get('/excluded-defaults/{locale}', fn () => '')->name('excluded.defaults');

$this->assertStringContainsString("url: '/excluded-defaults/{locale}'", $this->generate('excluded'));
}

public function test_route_middleware_defaults_win_over_global_middleware_defaults(): void
{
$this->app->afterResolving(Kernel::class, fn ($kernel) => $kernel->setGlobalMiddleware([
GlobalUrlDefaultsMiddleware::class,
]));

Route::middleware(UrlDefaultsMiddleware::class)
->get('/precedence/{locale}', fn () => '')->name('precedence.show');

$this->assertStringContainsString("@param locale - Default: 'en'", $this->generate('precedence'));
}
}
17 changes: 17 additions & 0 deletions workbench/app/Http/Middleware/GlobalUrlDefaultsMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Http\Middleware;

use Illuminate\Support\Facades\URL;

class GlobalUrlDefaultsMiddleware
{
public function handle($request, $next)
{
URL::defaults([
'locale' => 'global',
]);

return $next($request);
}
}
Loading