Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2f82308

Browse files
committedJun 18, 2025··
Return object from URI generation
This provides a richer API, allowing users to also retrieve the substitutions that were sent but aren't part of the route (either as a map or by generating a PSR-7 URI object). Signed-off-by: Luís Cobucci <[email protected]>
1 parent 39d1cc1 commit 2f82308

File tree

7 files changed

+270
-20
lines changed

7 files changed

+270
-20
lines changed
 

‎composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
],
1515
"require": {
1616
"php": ">=8.1.0",
17+
"psr/http-message": "^2.0",
1718
"psr/simple-cache": "^2.0 || ^3.0"
1819
},
1920
"require-dev": {
2021
"lcobucci/coding-standard": "^11.1",
22+
"nyholm/psr7": "^1.8",
2123
"phpbench/phpbench": "^1.2",
2224
"phpstan/extension-installer": "^1.1",
2325
"phpstan/phpstan": "^1.10",

‎composer.lock

Lines changed: 187 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/GenerateUri.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace FastRoute;
55

6+
use FastRoute\GenerateUri\GeneratedUri;
67
use FastRoute\GenerateUri\UriCouldNotBeGenerated;
78

89
/**
@@ -17,5 +18,5 @@ interface GenerateUri
1718
*
1819
* @throws UriCouldNotBeGenerated
1920
*/
20-
public function forRoute(string $name, array $substitutions = []): string;
21+
public function forRoute(string $name, array $substitutions = []): GeneratedUri;
2122
}

‎src/GenerateUri/FromProcessedConfiguration.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct(private readonly array $processedConfiguration)
2626
}
2727

2828
/** @inheritDoc */
29-
public function forRoute(string $name, array $substitutions = []): string
29+
public function forRoute(string $name, array $substitutions = []): GeneratedUri
3030
{
3131
if (! array_key_exists($name, $this->processedConfiguration)) {
3232
throw UriCouldNotBeGenerated::routeIsUndefined($name);
@@ -79,7 +79,7 @@ private function missingParameters(array $parts, array $substitutions): array
7979
* @param ParsedRoute $parsedRoute
8080
* @param UriSubstitutions $substitutions
8181
*/
82-
private function generatePath(string $route, array $parsedRoute, array $substitutions): string
82+
private function generatePath(string $route, array $parsedRoute, array $substitutions): GeneratedUri
8383
{
8484
$path = '';
8585

@@ -97,8 +97,11 @@ private function generatePath(string $route, array $parsedRoute, array $substitu
9797
}
9898

9999
$path .= $substitutions[$parameterName];
100+
unset($substitutions[$parameterName]);
100101
}
101102

102-
return $path;
103+
assert($path !== '');
104+
105+
return new GeneratedUri($path, $substitutions);
103106
}
104107
}

‎src/GenerateUri/GeneratedUri.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace FastRoute\GenerateUri;
5+
6+
use FastRoute\GenerateUri;
7+
use Psr\Http\Message\UriInterface;
8+
use Stringable;
9+
10+
use function http_build_query;
11+
12+
/** @phpstan-import-type UriSubstitutions from GenerateUri */
13+
final class GeneratedUri implements Stringable
14+
{
15+
/**
16+
* @param non-empty-string $path
17+
* @param UriSubstitutions $unmatchedSubstitutions
18+
*/
19+
public function __construct(
20+
public readonly string $path,
21+
public readonly array $unmatchedSubstitutions,
22+
) {
23+
}
24+
25+
public function asUri(UriInterface $baseUri): UriInterface
26+
{
27+
return $baseUri
28+
->withPath($this->path)
29+
->withQuery(http_build_query($this->unmatchedSubstitutions));
30+
}
31+
32+
public function __toString(): string
33+
{
34+
return $this->path;
35+
}
36+
}

‎test/FastRouteTest.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use FastRoute\Dispatcher;
99
use FastRoute\FastRoute;
1010
use FastRoute\GenerateUri;
11+
use Nyholm\Psr7\Uri;
1112
use PHPUnit\Framework\Attributes as PHPUnit;
1213
use PHPUnit\Framework\TestCase;
1314
use RuntimeException;
@@ -115,9 +116,9 @@ public function uriGeneratorCanBeOverridden(): void
115116
{
116117
$generator = new class () implements GenerateUri {
117118
/** @inheritDoc */
118-
public function forRoute(string $name, array $substitutions = []): string
119+
public function forRoute(string $name, array $substitutions = []): GenerateUri\GeneratedUri
119120
{
120-
return '';
121+
return new GenerateUri\GeneratedUri('/', $substitutions);
121122
}
122123
};
123124

@@ -159,10 +160,11 @@ public function processedDataShouldOnlyBeBuiltOnce(): void
159160
self::assertInstanceOf(Dispatcher\Result\Matched::class, $dispatcher->dispatch('POST', '/users/lcobucci'));
160161
self::assertInstanceOf(Dispatcher\Result\Matched::class, $dispatcher->dispatch('GET', '/posts/1234'));
161162

162-
self::assertSame('/users/lcobucci', $uriGenerator->forRoute('users', ['name' => 'lcobucci']));
163-
self::assertSame('/posts/1234', $uriGenerator->forRoute('posts.fetch', ['id' => '1234']));
164-
self::assertSame('/articles/2024', $uriGenerator->forRoute('articles.fetch', ['year' => '2024']));
165-
self::assertSame('/articles/2024/02', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02']));
166-
self::assertSame('/articles/2024/02/15', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15']));
163+
self::assertEquals('/users/lcobucci', $uriGenerator->forRoute('users', ['name' => 'lcobucci']));
164+
self::assertEquals('/posts/1234', $uriGenerator->forRoute('posts.fetch', ['id' => '1234']));
165+
self::assertEquals('/articles/2024', $uriGenerator->forRoute('articles.fetch', ['year' => '2024']));
166+
self::assertEquals('/articles/2024/02', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02']));
167+
self::assertEquals('/articles/2024/02/15', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15']));
168+
self::assertSame('/articles/2024/02/15?extra=value', (string) $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15', 'extra' => 'value'])->asUri(new Uri()));
167169
}
168170
}

‎test/GenerateUri/FromProcessedConfigurationTest.php

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use FastRoute\GenerateUri;
77
use FastRoute\RouteParser;
8+
use Nyholm\Psr7\Uri;
89
use PHPUnit\Framework\Attributes as PHPUnit;
910
use PHPUnit\Framework\TestCase;
1011

@@ -63,22 +64,22 @@ public function routesWithOptionalSegmentsCanBeGenerated(): void
6364
{
6465
$generator = self::routeGeneratorFor(['archive.fetch' => '/archive/{username}[/{year}[/{month}[/{day}]]]']);
6566

66-
self::assertSame(
67+
self::assertEquals(
6768
'/archive/test',
6869
$generator->forRoute('archive.fetch', ['username' => 'test']),
6970
);
7071

71-
self::assertSame(
72+
self::assertEquals(
7273
'/archive/test/2024',
7374
$generator->forRoute('archive.fetch', ['username' => 'test', 'year' => '2024']),
7475
);
7576

76-
self::assertSame(
77+
self::assertEquals(
7778
'/archive/test/2024/02',
7879
$generator->forRoute('archive.fetch', ['username' => 'test', 'year' => '2024', 'month' => '02']),
7980
);
8081

81-
self::assertSame(
82+
self::assertEquals(
8283
'/archive/test/2024/02/01',
8384
$generator->forRoute(
8485
'archive.fetch',
@@ -92,15 +93,15 @@ public function staticRoutesCanAlsoBeGenerated(): void
9293
{
9394
$generator = self::routeGeneratorFor(['post.fetch-special' => '/post/a-special-post']);
9495

95-
self::assertSame('/post/a-special-post', $generator->forRoute('post.fetch-special'));
96+
self::assertEquals('/post/a-special-post', $generator->forRoute('post.fetch-special'));
9697
}
9798

9899
#[PHPUnit\Test]
99100
public function resultingUriMustNotHaveUrlEncodedParameters(): void
100101
{
101102
$generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']);
102103

103-
self::assertSame(
104+
self::assertEquals(
104105
'/post/@something-that needs to be encoded 😁',
105106
$generator->forRoute('post.fetch', ['id' => '@something-that needs to be encoded 😁']),
106107
);
@@ -111,18 +112,37 @@ public function urlEncodedParametersShouldNotBeManipulated(): void
111112
{
112113
$generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']);
113114

114-
self::assertSame(
115+
self::assertEquals(
115116
'/post/%40something%20that%20needs%20to%20be%20encoded%20%F0%9F%98%81',
116117
$generator->forRoute('post.fetch', ['id' => '%40something%20that%20needs%20to%20be%20encoded%20%F0%9F%98%81']),
117118
);
118119
}
119120

121+
#[PHPUnit\Test]
122+
public function nonProcessedParametersAreRetrievable(): void
123+
{
124+
$generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']);
125+
$generatedUri = $generator->forRoute('post.fetch', ['id' => 'testing', 'foo' => 'bar', 'baz' => 'foo']);
126+
127+
self::assertSame('/post/testing', $generatedUri->path);
128+
self::assertSame(['foo' => 'bar', 'baz' => 'foo'], $generatedUri->unmatchedSubstitutions);
129+
self::assertSame(
130+
'https://api.my-company.dev:8080/post/testing?foo=bar&baz=foo',
131+
(string) $generatedUri->asUri(
132+
(new Uri())
133+
->withScheme('https')
134+
->withHost('api.my-company.dev')
135+
->withPort(8080),
136+
),
137+
);
138+
}
139+
120140
#[PHPUnit\Test]
121141
public function unicodeParametersAreAlsoAccepted(): void
122142
{
123143
$generator = self::routeGeneratorFor(['post.fetch' => '/post/{id:[\w\-\%]+}']);
124144

125-
self::assertSame('/post/bar-測試', $generator->forRoute('post.fetch', ['id' => 'bar-測試']));
145+
self::assertEquals('/post/bar-測試', $generator->forRoute('post.fetch', ['id' => 'bar-測試']));
126146
}
127147

128148
/** @param non-empty-array<non-empty-string, non-empty-string> $routeMap */

0 commit comments

Comments
 (0)
Please sign in to comment.