diff --git a/composer.json b/composer.json index fe36eb0..77306f3 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,12 @@ ], "require": { "php": ">=8.1.0", + "psr/http-message": "^2.0", "psr/simple-cache": "^2.0 || ^3.0" }, "require-dev": { "lcobucci/coding-standard": "^11.1", + "nyholm/psr7": "^1.8", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.10", diff --git a/composer.lock b/composer.lock index bdce53b..265312a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,61 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a670de9ab47a3fe8e1b4fc73254694e1", + "content-hash": "adc65739d5614b64fdd6f70dbf90d8ef", "packages": [ + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/simple-cache", "version": "3.0.0", @@ -513,6 +566,84 @@ }, "time": "2025-05-31T08:24:38+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -1613,6 +1744,61 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, { "name": "psr/log", "version": "3.0.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ea11052..1b28a22 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,6 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" colors="true" beStrictAboutCoverageMetadata="true" - beStrictAboutTodoAnnotatedTests="true" beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" displayDetailsOnTestsThatTriggerDeprecations="true" diff --git a/src/GenerateUri.php b/src/GenerateUri.php index 4291bcf..4a4d7d4 100644 --- a/src/GenerateUri.php +++ b/src/GenerateUri.php @@ -3,6 +3,7 @@ namespace FastRoute; +use FastRoute\GenerateUri\GeneratedUri; use FastRoute\GenerateUri\UriCouldNotBeGenerated; /** @@ -17,5 +18,5 @@ interface GenerateUri * * @throws UriCouldNotBeGenerated */ - public function forRoute(string $name, array $substitutions = []): string; + public function forRoute(string $name, array $substitutions = []): GeneratedUri; } diff --git a/src/GenerateUri/FromProcessedConfiguration.php b/src/GenerateUri/FromProcessedConfiguration.php index aae03b1..e90bda7 100644 --- a/src/GenerateUri/FromProcessedConfiguration.php +++ b/src/GenerateUri/FromProcessedConfiguration.php @@ -26,7 +26,7 @@ public function __construct(private readonly array $processedConfiguration) } /** @inheritDoc */ - public function forRoute(string $name, array $substitutions = []): string + public function forRoute(string $name, array $substitutions = []): GeneratedUri { if (! array_key_exists($name, $this->processedConfiguration)) { throw UriCouldNotBeGenerated::routeIsUndefined($name); @@ -79,7 +79,7 @@ private function missingParameters(array $parts, array $substitutions): array * @param ParsedRoute $parsedRoute * @param UriSubstitutions $substitutions */ - private function generatePath(string $route, array $parsedRoute, array $substitutions): string + private function generatePath(string $route, array $parsedRoute, array $substitutions): GeneratedUri { $path = ''; @@ -97,8 +97,11 @@ private function generatePath(string $route, array $parsedRoute, array $substitu } $path .= $substitutions[$parameterName]; + unset($substitutions[$parameterName]); } - return $path; + assert($path !== ''); + + return new GeneratedUri($path, $substitutions); } } diff --git a/src/GenerateUri/GeneratedUri.php b/src/GenerateUri/GeneratedUri.php new file mode 100644 index 0000000..c48341f --- /dev/null +++ b/src/GenerateUri/GeneratedUri.php @@ -0,0 +1,36 @@ +withPath($this->path) + ->withQuery(http_build_query($this->unmatchedSubstitutions)); + } + + public function __toString(): string + { + return $this->path; + } +} diff --git a/test/FastRouteTest.php b/test/FastRouteTest.php index 8afe5f5..46bad72 100644 --- a/test/FastRouteTest.php +++ b/test/FastRouteTest.php @@ -8,6 +8,7 @@ use FastRoute\Dispatcher; use FastRoute\FastRoute; use FastRoute\GenerateUri; +use Nyholm\Psr7\Uri; use PHPUnit\Framework\Attributes as PHPUnit; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -115,9 +116,9 @@ public function uriGeneratorCanBeOverridden(): void { $generator = new class () implements GenerateUri { /** @inheritDoc */ - public function forRoute(string $name, array $substitutions = []): string + public function forRoute(string $name, array $substitutions = []): GenerateUri\GeneratedUri { - return ''; + return new GenerateUri\GeneratedUri('/', $substitutions); } }; @@ -159,10 +160,11 @@ public function processedDataShouldOnlyBeBuiltOnce(): void self::assertInstanceOf(Dispatcher\Result\Matched::class, $dispatcher->dispatch('POST', '/users/lcobucci')); self::assertInstanceOf(Dispatcher\Result\Matched::class, $dispatcher->dispatch('GET', '/posts/1234')); - self::assertSame('/users/lcobucci', $uriGenerator->forRoute('users', ['name' => 'lcobucci'])); - self::assertSame('/posts/1234', $uriGenerator->forRoute('posts.fetch', ['id' => '1234'])); - self::assertSame('/articles/2024', $uriGenerator->forRoute('articles.fetch', ['year' => '2024'])); - self::assertSame('/articles/2024/02', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02'])); - self::assertSame('/articles/2024/02/15', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15'])); + self::assertEquals('/users/lcobucci', $uriGenerator->forRoute('users', ['name' => 'lcobucci'])); + self::assertEquals('/posts/1234', $uriGenerator->forRoute('posts.fetch', ['id' => '1234'])); + self::assertEquals('/articles/2024', $uriGenerator->forRoute('articles.fetch', ['year' => '2024'])); + self::assertEquals('/articles/2024/02', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02'])); + self::assertEquals('/articles/2024/02/15', $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15'])); + self::assertSame('/articles/2024/02/15?extra=value', (string) $uriGenerator->forRoute('articles.fetch', ['year' => '2024', 'month' => '02', 'day' => '15', 'extra' => 'value'])->asUri(new Uri())); } } diff --git a/test/GenerateUri/FromProcessedConfigurationTest.php b/test/GenerateUri/FromProcessedConfigurationTest.php index 704b8ab..57c2761 100644 --- a/test/GenerateUri/FromProcessedConfigurationTest.php +++ b/test/GenerateUri/FromProcessedConfigurationTest.php @@ -5,6 +5,7 @@ use FastRoute\GenerateUri; use FastRoute\RouteParser; +use Nyholm\Psr7\Uri; use PHPUnit\Framework\Attributes as PHPUnit; use PHPUnit\Framework\TestCase; @@ -63,22 +64,22 @@ public function routesWithOptionalSegmentsCanBeGenerated(): void { $generator = self::routeGeneratorFor(['archive.fetch' => '/archive/{username}[/{year}[/{month}[/{day}]]]']); - self::assertSame( + self::assertEquals( '/archive/test', $generator->forRoute('archive.fetch', ['username' => 'test']), ); - self::assertSame( + self::assertEquals( '/archive/test/2024', $generator->forRoute('archive.fetch', ['username' => 'test', 'year' => '2024']), ); - self::assertSame( + self::assertEquals( '/archive/test/2024/02', $generator->forRoute('archive.fetch', ['username' => 'test', 'year' => '2024', 'month' => '02']), ); - self::assertSame( + self::assertEquals( '/archive/test/2024/02/01', $generator->forRoute( 'archive.fetch', @@ -92,7 +93,7 @@ public function staticRoutesCanAlsoBeGenerated(): void { $generator = self::routeGeneratorFor(['post.fetch-special' => '/post/a-special-post']); - self::assertSame('/post/a-special-post', $generator->forRoute('post.fetch-special')); + self::assertEquals('/post/a-special-post', $generator->forRoute('post.fetch-special')); } #[PHPUnit\Test] @@ -100,7 +101,7 @@ public function resultingUriMustNotHaveUrlEncodedParameters(): void { $generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']); - self::assertSame( + self::assertEquals( '/post/@something-that needs to be encoded 😁', $generator->forRoute('post.fetch', ['id' => '@something-that needs to be encoded 😁']), ); @@ -111,18 +112,37 @@ public function urlEncodedParametersShouldNotBeManipulated(): void { $generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']); - self::assertSame( + self::assertEquals( '/post/%40something%20that%20needs%20to%20be%20encoded%20%F0%9F%98%81', $generator->forRoute('post.fetch', ['id' => '%40something%20that%20needs%20to%20be%20encoded%20%F0%9F%98%81']), ); } + #[PHPUnit\Test] + public function nonProcessedParametersAreRetrievable(): void + { + $generator = self::routeGeneratorFor(['post.fetch' => '/post/{id}']); + $generatedUri = $generator->forRoute('post.fetch', ['id' => 'testing', 'foo' => 'bar', 'baz' => 'foo']); + + self::assertSame('/post/testing', $generatedUri->path); + self::assertSame(['foo' => 'bar', 'baz' => 'foo'], $generatedUri->unmatchedSubstitutions); + self::assertSame( + 'https://api.my-company.dev:8080/post/testing?foo=bar&baz=foo', + (string) $generatedUri->asUri( + (new Uri()) + ->withScheme('https') + ->withHost('api.my-company.dev') + ->withPort(8080), + ), + ); + } + #[PHPUnit\Test] public function unicodeParametersAreAlsoAccepted(): void { $generator = self::routeGeneratorFor(['post.fetch' => '/post/{id:[\w\-\%]+}']); - self::assertSame('/post/bar-測試', $generator->forRoute('post.fetch', ['id' => 'bar-測試'])); + self::assertEquals('/post/bar-測試', $generator->forRoute('post.fetch', ['id' => 'bar-測試'])); } /** @param non-empty-array $routeMap */