From a24f9e31c287e32c7facd1d3cf86c88f045d7951 Mon Sep 17 00:00:00 2001 From: Joshua Estes Date: Sun, 12 Nov 2023 18:25:23 -0500 Subject: [PATCH 1/2] [HttpMessage] Helper Methods on Uri to make it easier to create Uri objects with query params --- .../Component/HttpMessage/Tests/UriTest.php | 85 ++++++++++++++----- src/SonsOfPHP/Component/HttpMessage/Uri.php | 69 ++++++++++++++- 2 files changed, 133 insertions(+), 21 deletions(-) diff --git a/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php b/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php index d3ba5379..2cbff6e9 100644 --- a/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php +++ b/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php @@ -23,6 +23,69 @@ public function testItImplementsCorrectInterface(): void $this->assertInstanceOf(UriInterface::class, new Uri()); } + /** + * @covers ::withQueryParam + */ + public function testWithQueryParam(): void + { + $uri = new Uri('https://docs.sonsofphp.com'); + $this->assertNotSame($uri, $uri->withQueryParam('page', 1)); + $this->assertSame('page=1', $uri->withQueryParam('page', 1)->getQuery()); + } + + /** + * @covers ::withQueryParams + */ + public function testWithQueryParams(): void + { + $uri = new Uri('https://docs.sonsofphp.com'); + $this->assertNotSame($uri, $uri->withQueryParams(['page' => 1])); + $this->assertSame('page=1', $uri->withQueryParams(['page' => 1])->getQuery()); + } + + /** + * @covers ::getQuery + */ + public function testGetQueryWorksAsExpectedWhenComplexQueryParams(): void + { + $uri = new Uri(); + $this->assertSame( + 'search=search%20term&filters[active]=1', + $uri->withQueryParams([ + 'search' => 'search term', + 'filters' => [ + 'active' => '1', + ], + ])->getQuery()); + } + + /** + * @covers ::getQuery + */ + public function testGetQueryWorksAsExpected(): void + { + $uri = new Uri('https://docs.sonsofphp.com'); + $this->assertSame('', $uri->getQuery()); + + $uri = new Uri('https://docs.sonsofphp.com?q'); + $this->assertSame('q', $uri->getQuery()); + + $uri = new Uri('https://docs.sonsofphp.com?q=test%20query'); + $this->assertSame('q=test%20query', $uri->getQuery()); + + $uri = new Uri('https://docs.sonsofphp.com?page=1&limit=100'); + $this->assertSame('page=1&limit=100', $uri->getQuery()); + } + + /** + * @covers ::__construct + */ + public function testConstructWithQuery(): void + { + $uri = new Uri('https://docs.sonsofphp.com?q=test'); + $this->assertSame('q=test', $uri->getQuery()); + } + /** * @covers ::__construct */ @@ -108,24 +171,6 @@ public function testGetPathWorksAsExpected(): void $this->assertSame('/components', $uri->getPath()); } - /** - * @covers ::getQuery - */ - public function testGetQueryWorksAsExpected(): void - { - $uri = new Uri('https://docs.sonsofphp.com'); - $this->assertSame('', $uri->getQuery()); - - $uri = new Uri('https://docs.sonsofphp.com?q'); - $this->assertSame('q', $uri->getQuery()); - - $uri = new Uri('https://docs.sonsofphp.com?q=test%20query'); - $this->assertSame('q=test%20query', $uri->getQuery()); - - $uri = new Uri('https://docs.sonsofphp.com?page=1&limit=100'); - $this->assertSame('page=1&limit=100', $uri->getQuery()); - } - /** * @covers ::getFragment */ @@ -224,8 +269,10 @@ public function testWithPathWorksAsExpected(): void public function testWithQueryWorksAsExpected(): void { $uri = new Uri('https://docs.sonsofphp.com'); - $this->assertNotSame($uri, $uri->withQuery('/test')); + $this->assertNotSame($uri, $uri->withQuery('test=yes')); $this->assertSame($uri, $uri->withQuery('')); + + $this->assertSame('testing=yes', $uri->withQuery('testing=yes')->getQuery()); } /** diff --git a/src/SonsOfPHP/Component/HttpMessage/Uri.php b/src/SonsOfPHP/Component/HttpMessage/Uri.php index 39f00c47..ad985b63 100644 --- a/src/SonsOfPHP/Component/HttpMessage/Uri.php +++ b/src/SonsOfPHP/Component/HttpMessage/Uri.php @@ -21,6 +21,7 @@ class Uri implements UriInterface, \Stringable private ?string $password; private ?string $query; private ?string $fragment; + private array $queryParams = []; public function __construct( private string $uri = '', @@ -36,6 +37,10 @@ public function __construct( $this->path = $parts['path'] ?? null; $this->query = $parts['query'] ?? null; $this->fragment = $parts['fragment'] ?? null; + + if (!empty($parts['query'])) { + parse_str($parts['query'], $this->queryParams); + } } } @@ -113,7 +118,29 @@ public function getPath(): string */ public function getQuery(): string { - return $this->query ?? ''; + //return http_build_query($this->queryParams, '', null, \PHP_QUERY_RFC3986); + + $query = ''; + foreach ($this->queryParams as $key => $value) { + if (is_array($value)) { + foreach ($value as $n => $v) { + $query .= sprintf('&%s[%s]', $key, $n); + if (!empty($v)) { + $query .= '=' . rawurlencode((string) $v); + } + } + continue; + } + + $query .= '&' . $key; + + // null or '' + if (!empty($value)) { + $query .= '=' . rawurlencode((string) $value); + continue; + } + } + return ltrim($query, '&'); } /** @@ -218,9 +245,12 @@ public function withQuery(string $query): UriInterface return $this; } + parse_str($query, $output); + $that = clone $this; - $that->query = $query; + //$that->query = $query; + $that->queryParams = $output; return $that; } @@ -256,4 +286,39 @@ public function __toString(): string ($this->fragment ? '#' . $this->fragment : '') ; } + + /** + * Example: + * ->withQueryParams([ + * 'query' => 'search string', + * 'page' => '1', + * 'limit' => '10', + * 'filters' => [ + * 'active' => '1', + * ], + * ]); + * ?query=search%20string&page=1&limit=10&filters[active]=1 + */ + public function withQueryParams(array $params): static + { + $that = clone $this; + + $that->queryParams = $params; + + return $that; + } + + /** + * Examples + * ->withQueryParam('page', 1); + * ?page=1 + */ + public function withQueryParam(string $name, int|string|array $value): static + { + $that = clone $this; + + $that->queryParams[$name] = $value; + + return $that; + } } From d3ebf420a56a1d164352f0086845364aa9cfc6d1 Mon Sep 17 00:00:00 2001 From: Joshua Estes Date: Sun, 12 Nov 2023 18:39:30 -0500 Subject: [PATCH 2/2] updates --- CHANGELOG.md | 1 + docs/components/http-message/index.md | 31 ++++++++++++++++++- .../Component/HttpMessage/Tests/UriTest.php | 21 ++++++++++++- src/SonsOfPHP/Component/HttpMessage/Uri.php | 9 ++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a95b95b..e31c71d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Examples: * [PR #59](https://github.com/SonsOfPHP/sonsofphp/pull/59) Added new HttpFactory component * [PR #70](https://github.com/SonsOfPHP/sonsofphp/pull/70) Added new Core contract * [PR #112](https://github.com/SonsOfPHP/sonsofphp/pull/112) [Cache] Added new component +* [PR #119](https://github.com/SonsOfPHP/sonsofphp/pull/119) [HttpMessage] Added `withQueryParams` and `withQueryParam` to `Uri` ## [0.3.8] diff --git a/docs/components/http-message/index.md b/docs/components/http-message/index.md index b279335a..10d2c9d8 100644 --- a/docs/components/http-message/index.md +++ b/docs/components/http-message/index.md @@ -1,5 +1,6 @@ --- -title: Http Message +title: Http Message (PSR-7) +description: PHP PSR-7 implementation --- # Http Message Component @@ -11,3 +12,31 @@ Simple PSR-7 Compatible Http Message Component ```shell composer require sonsofphp/http-message ``` + +## Usage + +### Uri + +```php +withQueryParams([ + 'page' => 1, + 'limit' => 25, + 'filters' => [ + 'isActive' => 1, + ] +]); + +// To remove all the query parameters, pass in `null` +$uri = $uri->withQueryParams(null); +``` diff --git a/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php b/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php index 2cbff6e9..c20b2192 100644 --- a/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php +++ b/src/SonsOfPHP/Component/HttpMessage/Tests/UriTest.php @@ -33,6 +33,24 @@ public function testWithQueryParam(): void $this->assertSame('page=1', $uri->withQueryParam('page', 1)->getQuery()); } + /** + * @covers ::withQueryParams + */ + public function testWithQueryParamsCanBeUsedToRemove(): void + { + $uri = new Uri('https://docs.sonsofphp.com?page=1&limit=100'); + $this->assertSame('', $uri->withQueryParams(null)->getQuery()); + } + + /** + * @covers ::withQueryParams + */ + public function testWithQueryParamsWillAdd(): void + { + $uri = new Uri('https://docs.sonsofphp.com'); + $this->assertSame('page=1&limit=100', $uri->withQueryParams(['page' => 1])->withQueryParams(['limit' => 100])->getQuery()); + } + /** * @covers ::withQueryParams */ @@ -56,7 +74,8 @@ public function testGetQueryWorksAsExpectedWhenComplexQueryParams(): void 'filters' => [ 'active' => '1', ], - ])->getQuery()); + ])->getQuery() + ); } /** diff --git a/src/SonsOfPHP/Component/HttpMessage/Uri.php b/src/SonsOfPHP/Component/HttpMessage/Uri.php index ad985b63..111afb50 100644 --- a/src/SonsOfPHP/Component/HttpMessage/Uri.php +++ b/src/SonsOfPHP/Component/HttpMessage/Uri.php @@ -299,11 +299,16 @@ public function __toString(): string * ]); * ?query=search%20string&page=1&limit=10&filters[active]=1 */ - public function withQueryParams(array $params): static + public function withQueryParams(?array $params): static { $that = clone $this; - $that->queryParams = $params; + if (null === $params) { + $that->queryParams = []; + return $that; + } + + $that->queryParams += $params; return $that; }