Skip to content

Commit

Permalink
Fixes route() base uri, query parameters and domain
Browse files Browse the repository at this point in the history
  • Loading branch information
nunomaduro committed Aug 16, 2023
1 parent eceb36b commit ac208fb
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 23 deletions.
2 changes: 1 addition & 1 deletion src/Console/ListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ protected function routesFromMountPaths(array $mountPaths): Collection
protected function routeName(string $mountPath, string $viewPath): ?string
{
return collect($this->laravel->make(FolioRoutes::class)->routes())->search(function (array $route) use ($mountPath, $viewPath) {
[$routeRelativeMountPath, $routeRelativeViewPath] = $route;
['mountPath' => $routeRelativeMountPath, 'path' => $routeRelativeViewPath] = $route;

return $routeRelativeMountPath === Project::relativePathOf($mountPath)
&& $routeRelativeViewPath === Project::relativePathOf($viewPath);
Expand Down
78 changes: 59 additions & 19 deletions src/FolioRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

class FolioRoutes
{
/**
* The current version of the persisted routes.
*/
protected static int $version = 1;

/**
* Create a new Folio routes instance.
*
Expand All @@ -41,7 +46,10 @@ public function persist(): void

File::put(
$this->cachedFolioRoutesPath,
'<?php return '.var_export($this->routes, true).';',
'<?php return '.var_export([
'version' => static::$version,
'routes' => $this->routes,
], true).';',
);
}

Expand All @@ -67,9 +75,15 @@ protected function load(): void
}

if (File::exists($this->cachedFolioRoutesPath)) {
$this->routes = File::getRequire($this->cachedFolioRoutesPath);
$cache = File::getRequire($this->cachedFolioRoutesPath);

return;
if (isset($cache['version']) && (int) $cache['version'] === static::$version) {
$this->routes = $cache['routes'];

$this->loaded = true;

return;
}
}

foreach ($this->manager->mountPaths() as $mountPath) {
Expand All @@ -80,8 +94,10 @@ protected function load(): void

if ($name = $matchedView->name()) {
$this->routes[$name] = [
Project::relativePathOf($matchedView->mountPath),
Project::relativePathOf($matchedView->path),
'mountPath' => Project::relativePathOf($matchedView->mountPath),
'path' => Project::relativePathOf($matchedView->path),
'baseUri' => $mountPath->baseUri,
'domain' => $mountPath->domain,
];
}
}
Expand All @@ -102,6 +118,8 @@ public function has(string $name): bool

/**
* Get the route URL for the given route name and arguments.
*
* @thows \Laravel\Folio\Exceptions\UrlGenerationException
*/
public function get(string $name, array $arguments, bool $absolute): string
{
Expand All @@ -111,50 +129,72 @@ public function get(string $name, array $arguments, bool $absolute): string
throw new RouteNotFoundException("Route [{$name}] not found.");
}

[$mountPath, $path] = $this->routes[$name];
[
'mountPath' => $mountPath,
'path' => $path,
'baseUri' => $baseUri,
'domain' => $domain,
] = $this->routes[$name];

return with($this->path($mountPath, $path, $arguments), function (string $path) use ($absolute) {
return $absolute ? url($path) : $path;
});
[$path, $remainingArguments] = $this->path($mountPath, $path, $arguments);

$route = new Route(['GET'], '{__folio_path}', fn () => null);

$route->name($name)->domain($domain);

$uri = $baseUri === '/' ? $path : $baseUri.$path;

try {
return url()->toRoute($route, [...$remainingArguments, '__folio_path' => $uri], $absolute);
} catch (\Illuminate\Routing\Exceptions\UrlGenerationException $e) {
throw new UrlGenerationException(str_replace('{__folio_path}', $uri, $e->getMessage()), $e->getCode(), $e);
}
}

/**
* Get the relative route URL for the given route name and arguments.
*
* @param array<string, mixed> $parameters
* @return array{string, array<string, mixed>}
*/
protected function path(string $mountPath, string $path, array $parameters): string
protected function path(string $mountPath, string $path, array $parameters): array
{
$uri = str_replace('.blade.php', '', $path);

$parameters = collect($parameters);
$usedParameters = collect();

$uri = collect(explode('/', $uri))
->map(function (string $segment) use ($parameters, $uri) {
->map(function (string $segment) use ($parameters, $uri, $usedParameters) {
if (! Str::startsWith($segment, '[')) {
return $segment;
}

$segment = new PotentiallyBindablePathSegment($segment);

$parameters = collect($parameters)->mapWithKeys(function (mixed $value, string $key) {
return [Str::camel($key) => $value];
})->all();
$name = $segment->variable();

if (! isset($parameters[$name = $segment->variable()]) || $parameters[$name] === null) {
throw UrlGenerationException::forMissingParameter($uri, $name);
}
$value = $parameters->first(function (mixed $value, string $key) use ($name) {
return Str::camel($key) === $name && $value !== null;
}, fn () => throw UrlGenerationException::forMissingParameter($uri, $name));

$usedParameters->put($name, $value);

return $this->formatParameter(
$uri,
$name,
$parameters[$name],
$value,
$segment->field(),
$segment->capturesMultipleSegments()
);
})->implode('/');

$uri = str_replace(['/index', '/index/'], ['', '/'], $uri);

return '/'.ltrim(substr($uri, strlen($mountPath)), '/');
return [
'/'.ltrim(substr($uri, strlen($mountPath)), '/'),
$parameters->except($usedParameters->keys()->all())->all(),
];
}

/**
Expand Down
20 changes: 20 additions & 0 deletions tests/Feature/NameTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,23 @@
test('routes may not have a name', function () {
route('users.index');
})->throws(RouteNotFoundException::class, 'Route [users.index] not defined.');

test('custom uri', function () {
Folio::uri('/user')->path(__DIR__.'/resources/views/even-more-pages');

$absoluteRoute = route('profile');
$route = route('profile', [], false);

expect($absoluteRoute)->toBe('http://localhost/user/profile')
->and($route)->toBe('/user/profile');
});

test('custom domain', function () {
Folio::domain('example.com')->uri('/user')->path(__DIR__.'/resources/views/even-more-pages');

$absoluteRoute = route('profile');
$route = route('profile', [], false);

expect($absoluteRoute)->toBe('http://example.com/user/profile')
->and($route)->toBe('/user/profile');
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<div>
My profile
</div>

<?php
Laravel\Folio\name('profile');
68 changes: 65 additions & 3 deletions tests/Unit/FolioRoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@
$arguments = collect($arguments)->map(fn ($argument) => value($argument))->all();

$names = new FolioRoutes(Mockery::mock(FolioManager::class), '', [
$name => [$mountPath, $viewPath],
$name => [
'mountPath' => $mountPath,
'path' => $viewPath,
'baseUri' => '/',
'domain' => null,
],
], true);

expect($names->has($name))->toBeTrue()
->and($names->get($name, $arguments, false))->toBe($expectedRoute);
})->with(fn () => collect([
'podcasts.index' => ['podcasts/index.blade.php', [], '/podcasts'],
'podcasts.index-with-query-parameters' => ['podcasts/index.blade.php', ['page' => 1], '/podcasts?page=1'],
'podcasts.show-by-id' => ['podcasts/[id].blade.php', ['id' => 1], '/podcasts/1'],
'podcasts.show-by-name' => ['podcasts/[name].blade.php', ['name' => 'Taylor'], '/podcasts/Taylor'],
'podcasts.show-by-slug' => ['podcasts/[slug].blade.php', ['slug' => 'nuno'], '/podcasts/nuno'],
Expand All @@ -49,6 +55,7 @@
'podcasts.show-by-backed-enum' => ['podcasts/[Category].blade.php', ['category' => Category::Post], '/podcasts/posts'],
'podcasts.show-by-id-with-nested-page' => ['podcasts/[id]/stats.blade.php', ['id' => 1], '/podcasts/1/stats'],
'podcasts.stats' => ['podcasts/stats.blade.php', [], '/podcasts/stats'],
'podcasts.stats-with-query-parameters' => ['podcasts/stats.blade.php', ['page' => 1, 'lowerCase' => 'lowerCaseKeyValue', 'Upper_case-key' => 'Upper_caseKeyValue'], '/podcasts/stats?page=1&lowerCase=lowerCaseKeyValue&Upper_case-key=Upper_caseKeyValue'],
'podcasts.many-by-id' => ['podcasts/[...id].blade.php', ['ids' => [1, 2, 3]], '/podcasts/1/2/3'],
'podcasts.many-by-name' => ['podcasts/[...name].blade.php', ['names' => ['Taylor', 'Nuno']], '/podcasts/Taylor/Nuno'],
'podcasts.many-by-slug' => ['podcasts/[...slug].blade.php', ['slugs' => ['nuno', 'taylor']], '/podcasts/nuno/taylor'],
Expand All @@ -59,6 +66,9 @@
'podcasts.many-by-model-name-2' => ['podcasts/[...Podcast-name].blade.php', ['podcasts' => fn () => Podcast::all()], '/podcasts/test-podcast-name-1/test-podcast-name-2'],
'podcasts.many-by-backed-enum' => ['podcasts/[...Category].blade.php', ['categories' => [Category::Post, Category::Video]], '/podcasts/posts/video'],
'podcasts.many-by-id-with-nested-page' => ['podcasts/[...id]/stats.blade.php', ['ids' => [1, 2, 3]], '/podcasts/1/2/3/stats'],
'articles.query-parameter' => ['articles.blade.php', ['page' => 1], '/articles?page=1'],
'articles.query-parameters' => ['articles.blade.php', ['page' => 1, 'lowerCase' => 'lowerCaseKeyValue', 'Upper_case-key' => 'Upper_caseKeyValue'], '/articles?page=1&lowerCase=lowerCaseKeyValue&Upper_case-key=Upper_caseKeyValue'],
'articles.query-array-parameter' => ['articles.blade.php', ['page' => [1, 2]], '/articles?page%5B0%5D=1&page%5B1%5D=2'],
])->map(function (array $value) {
$mountPath = 'resources/views/pages';

Expand All @@ -67,9 +77,56 @@
return [$mountPath, $mountPath.'/'.$viewRelativePath, $arguments, $expectedRoute];
})->mapWithKeys(fn (array $value, string $key) => [$key => [$key, $value]])->toArray());

it('may have absolute routes', function (string $name, array $scenario) {
[$mountPath, $viewPath, $domain, $arguments, $expectedRoute] = $scenario;

$arguments = collect($arguments)->map(fn ($argument) => value($argument))->all();

$names = new FolioRoutes(Mockery::mock(FolioManager::class), '', [
$name => [
'mountPath' => $mountPath,
'path' => $viewPath,
'baseUri' => '/',
'domain' => $domain,
],
], true);

expect($names->has($name))->toBeTrue()
->and($names->get($name, $arguments, true))->toBe($expectedRoute);
})->with(fn () => collect([
'podcasts.index' => ['podcasts/index.blade.php', 'domain.com', [], 'http://domain.com/podcasts'],
'podcasts.show' => ['podcasts/[id].blade.php', 'domain.com', ['id' => 1], 'http://domain.com/podcasts/1'],
'podcasts.show-by-account-and-name' => ['podcasts/[name].blade.php', '{account}.domain.com', ['account' => 'taylor', 'name' => 'Taylor'], 'http://taylor.domain.com/podcasts/Taylor'],
])->map(function (array $value) {
$mountPath = 'resources/views/pages';

[$viewRelativePath, $domain, $arguments, $expectedRoute] = $value;

return [$mountPath, $mountPath.'/'.$viewRelativePath, $domain, $arguments, $expectedRoute];
})->mapWithKeys(fn (array $value, string $key) => [$key => [$key, $value]])->toArray());

test('precedence', function () {
$names = new FolioRoutes(Mockery::mock(FolioManager::class), '', [
'podcasts.show' => [
'mountPath' => 'resources/views/pages',
'path' => 'resources/views/pages/podcasts/[id].blade.php',
'baseUri' => '/',
'domain' => null,
],
], true);

expect(fn () => $names->get('podcasts.show', ['id' => '{name}', 'name' => 'foo'], false))
->toThrow(UrlGenerationException::class, 'Missing required parameter for [Route: podcasts.show] [URI: /podcasts/{name}] [Missing parameter: name].');
});

it('may not have routes', function () {
$names = new FolioRoutes(Mockery::mock(FolioManager::class), '', [
'podcasts.index' => ['resources/views/pages', 'resources/views/pages/podcasts/index.blade.php'],
'podcasts.index' => [
'mountPath' => 'resources/views/pages',
'path' => 'resources/views/pages/podcasts/index.blade.php',
'baseUri' => '/',
'domain' => null,
],
], true);

expect($names->has('podcasts.show'))->toBeFalse()
Expand All @@ -78,7 +135,12 @@

it('can not have missing parameters', function () {
$names = new FolioRoutes(Mockery::mock(FolioManager::class), '', [
'podcasts.show' => ['resources/views/pages', 'resources/views/pages/podcasts/[id].blade.php'],
'podcasts.show' => [
'mountPath' => 'resources/views/pages',
'path' => 'resources/views/pages/podcasts/[id].blade.php',
'baseUri' => '/',
'domain' => null,
],
], true);

expect(fn () => $names->get('podcasts.show', [], false))
Expand Down

0 comments on commit ac208fb

Please sign in to comment.