From 2ec58b6f52b06426d28f77da1134b5bab7a053da Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 1 Dec 2024 18:27:00 -0600 Subject: [PATCH 01/15] work on uris --- src/Illuminate/Support/Uri.php | 225 ++++++++++++++++++++++ src/Illuminate/Support/UriQueryString.php | 88 +++++++++ 2 files changed, 313 insertions(+) create mode 100644 src/Illuminate/Support/Uri.php create mode 100644 src/Illuminate/Support/UriQueryString.php diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php new file mode 100644 index 000000000000..a5180fb42c92 --- /dev/null +++ b/src/Illuminate/Support/Uri.php @@ -0,0 +1,225 @@ +uri = $uri instanceof UriInterface ? $uri : LeagueUri::new((string) $uri); + } + + /** + * Create a new URI instance. + */ + public static function of(UriInterface|Stringable|string $uri = ''): static + { + return new static($uri); + } + + /** + * Get the URI's scheme. + */ + public function scheme(): ?string + { + return $this->uri->getScheme(); + } + + /** + * Get the user from the URI. + */ + public function user(bool $withPassword = false): ?string + { + if ($withPassword) { + return $this->uri->getUserInfo(); + } + + $userInfo = $this->uri->getUserInfo(); + + if (is_null($userInfo)) { + return null; + } + + return str_contains($userInfo, ':') + ? Str::before($userInfo, ':') + : $userInfo; + } + + /** + * Get the password from the URI. + */ + public function password(): ?string + { + $userInfo = $this->uri->getUserInfo(); + + return ! is_null($userInfo) ? Str::after($userInfo, ':') : null; + } + + /** + * Get the URI's host. + */ + public function host(): ?string + { + return $this->uri->getHost(); + } + + /** + * Get the URI's port. + */ + public function port(): ?int + { + return $this->uri->getPort(); + } + + /** + * Get the URI's path. + * + * Empty or missing paths are returned as a single "/". + */ + public function path(): ?string + { + $path = trim((string) $this->uri->getPath(), '/'); + + return $path === '' ? '/' : $path; + } + + /** + * Get the URI's query string. + */ + public function query(): UriQueryString + { + return new UriQueryString($this); + } + + /** + * Get the URI's fragment. + */ + public function fragment(): ?string + { + return $this->uri->getFragment(); + } + + /** + * Specify the scheme of the URI. + */ + public function withScheme(Stringable|string $scheme): static + { + return new static($this->uri->withScheme($scheme)); + } + + /** + * Specify the user and password for the URI. + */ + public function withUser(Stringable|string|null $user, #[SensitiveParameter] Stringable|string|null $password = null): static + { + return new static($this->uri->withUserInfo($user, $password)); + } + + /** + * Specify the host of the URI. + */ + public function withHost(Stringable|string $host): static + { + return new static($this->uri->withHost($host)); + } + + /** + * Specify the port of the URI. + */ + public function withPort(int|null $port): static + { + return new static($this->uri->withPort($port)); + } + + /** + * Specify the path of the URI. + */ + public function withPath(Stringable|string $path): static + { + return new static($this->uri->withPath(Str::start((string) $path, '/'))); + } + + /** + * Merge new query parameters into the URI. + */ + public function withQuery(Stringable|array|string $query, bool $merge = true): static + { + [$newQuery, $currentQuery] = [[], []]; + + if ($query instanceof Stringable || is_string($query)) { + parse_str($query, (string) $newQuery); + } else { + $newQuery = $query; + } + + foreach ($newQuery as $key => $parameter) { + if ($parameter instanceof UrlRoutable) { + $newQuery[$key] = $parameter->getRouteKey(); + } + } + + if ($merge) { + parse_str($this->uri->getQuery(), $currentQuery); + + $newQuery = array_merge($currentQuery, $newQuery); + } + + return new static($this->uri->withQuery(Arr::query($newQuery))); + } + + /** + * Specify new query parameters for the URI. + */ + public function replaceQuery(Stringable|array|string $query) + { + return $this->withQuery($query, merge: false); + } + + /** + * Specify the fragment of the URI. + */ + public function withFragment(string $fragment): static + { + return new static($this->uri->withFragment($fragment)); + } + + /** + * Get the underlying URI instance. + */ + public function getUri(): UriInterface + { + return $this->uri; + } + + /** + * Get content as a string of HTML. + * + * @return string + */ + public function toHtml() + { + return (string) $this; + } + + /** + * Get the string representation of the URI. + */ + public function __toString(): string + { + return $this->uri->toString(); + } +} diff --git a/src/Illuminate/Support/UriQueryString.php b/src/Illuminate/Support/UriQueryString.php new file mode 100644 index 000000000000..1dd0b697a89c --- /dev/null +++ b/src/Illuminate/Support/UriQueryString.php @@ -0,0 +1,88 @@ +uri = $uri; + } + + /** + * Retrieve all data from the instance. + * + * @param array|mixed|null $keys + * @return array + */ + public function all($keys = null) + { + $query = $this->toArray(); + + if (! $keys) { + return $query; + } + + $results = []; + + foreach (is_array($keys) ? $keys : func_get_args() as $key) { + Arr::set($results, $key, Arr::get($query, $key)); + } + + return $results; + } + + /** + * Retrieve data from the instance. + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + protected function data($key = null, $default = null) + { + return $this->get($key, $default); + } + + /** + * Get a query string parameter. + */ + public function get(?string $key = null, mixed $default = null): mixed + { + return data_get($this->toArray(), $key, $default); + } + + /** + * Get the URL decoded version of the query string. + */ + public function decode(): string + { + return rawurldecode((string) $this); + } + + /** + * Convert the query string into an array. + */ + public function toArray() + { + parse_str($this->uri->getUri()->getQuery(), $currentQuery); + + return $currentQuery; + } + + /** + * Get the string representation of the query string. + */ + public function __toString(): string + { + return (string) $this->uri->getUri()->getQuery(); + } +} From 99ded5be1f4f816b36fee0924d89619ad8213dc3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 1 Dec 2024 18:36:13 -0600 Subject: [PATCH 02/15] work on uris --- .../Providers/FoundationServiceProvider.php | 12 ++++++ src/Illuminate/Support/Uri.php | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index c137442a9b12..5e84a62f7201 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -27,6 +27,7 @@ use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Defer\DeferredCallbackCollection; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Uri; use Illuminate\Testing\LoggedExceptionCollection; use Illuminate\Testing\ParallelTestingServiceProvider; use Illuminate\Validation\ValidationException; @@ -89,6 +90,7 @@ public function register() $this->registerDumper(); $this->registerRequestValidation(); $this->registerRequestSignatureValidation(); + $this->registerUriUrlGeneration(); $this->registerDeferHandler(); $this->registerExceptionTracking(); $this->registerExceptionRenderer(); @@ -188,6 +190,16 @@ public function registerRequestSignatureValidation() }); } + /** + * Register the "defer" function termination handler. + * + * @return void + */ + protected function registerUriUrlGeneration() + { + Uri::setUrlGeneratorResolver(fn () => app('url')); + } + /** * Register the "defer" function termination handler. * diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index a5180fb42c92..df720d2dada2 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -2,6 +2,7 @@ namespace Illuminate\Support; +use Closure; use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Support\Traits\Conditionable; @@ -15,6 +16,11 @@ class Uri implements Htmlable { use Conditionable, Tappable; + /** + * The URL generator resolver. + */ + protected static ?Closure $urlGeneratorResolver = null; + /** * Create a new parsed URI instance. */ @@ -31,6 +37,29 @@ public static function of(UriInterface|Stringable|string $uri = ''): static return new static($uri); } + /** + * Generate an absolute URL to the given path. + */ + public static function to(string $path): static + { + return new static(call_user_func(static::$urlGeneratorResolver)->to($path)); + } + + /** + * Get a URI instance for a named route. + * + * @param \BackedEnum|string $name + * @param mixed $parameters + * @param bool $absolute + * @return static + * + * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException|\InvalidArgumentException + */ + public static function route($name, $parameters = [], $absolute = true): static + { + return new static(call_user_func(static::$urlGeneratorResolver)->route($name, $parameters, $absolute)); + } + /** * Get the URI's scheme. */ @@ -197,6 +226,14 @@ public function withFragment(string $fragment): static return new static($this->uri->withFragment($fragment)); } + /** + * Set the URL generator resolver. + */ + public static function setUrlGeneratorResolver(Closure $urlGeneratorResolver): void + { + static::$urlGeneratorResolver = $urlGeneratorResolver; + } + /** * Get the underlying URI instance. */ From ca285865a75a32e11049c18c88b062871b4c5dba Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 07:59:30 -0800 Subject: [PATCH 03/15] add uri method to request --- src/Illuminate/Http/Request.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index 40a61eff5736..821c2ee8defd 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use Illuminate\Support\Uri; use RuntimeException; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; use Symfony\Component\HttpFoundation\InputBag; @@ -88,6 +89,16 @@ public function method() return $this->getMethod(); } + /** + * Get a URI instance for the request. + * + * @return \Illuminate\Support\Uri + */ + public function uri() + { + return Uri::of($this->fullUrl()); + } + /** * Get the root URL for the application. * From 1462fcb39f0efa97fa723bebd39497a34d3e5e56 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 10:11:26 -0800 Subject: [PATCH 04/15] work on uris --- src/Illuminate/Support/Uri.php | 65 ++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index df720d2dada2..7f0d4af22bcf 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -38,7 +38,7 @@ public static function of(UriInterface|Stringable|string $uri = ''): static } /** - * Generate an absolute URL to the given path. + * Get a URI instance of an absolute URL for the given path. */ public static function to(string $path): static { @@ -185,35 +185,72 @@ public function withPath(Stringable|string $path): static /** * Merge new query parameters into the URI. */ - public function withQuery(Stringable|array|string $query, bool $merge = true): static + public function withQuery(array $query, bool $merge = true): static { - [$newQuery, $currentQuery] = [[], []]; + foreach ($query as $key => $value) { + if ($value instanceof UrlRoutable) { + $query[$key] = $value->getRouteKey(); + } + } + + if ($merge) { + $mergedQuery = $this->query()->all(); + + foreach ($query as $key => $value) { + data_set($mergedQuery, $key, $value); + } - if ($query instanceof Stringable || is_string($query)) { - parse_str($query, (string) $newQuery); + $newQuery = $mergedQuery; } else { $newQuery = $query; } - foreach ($newQuery as $key => $parameter) { - if ($parameter instanceof UrlRoutable) { - $newQuery[$key] = $parameter->getRouteKey(); + return new static($this->uri->withQuery(Arr::query($newQuery))); + } + + /** + * Merge new query parameters into the URI if they are not already in the query string. + */ + public function withQueryIfMissing(array $query): static + { + $currentQuery = $this->query(); + + foreach ($query as $key => $value) { + if (! $currentQuery->missing($key)) { + Arr::forget($query, $key); } } - if ($merge) { - parse_str($this->uri->getQuery(), $currentQuery); + return $this->withQuery($query); + } - $newQuery = array_merge($currentQuery, $newQuery); - } + /** + * Push a value onto the end of a query string parameter that is a list. + */ + public function pushOntoQuery(string $key, mixed $value): static + { + $currentValue = data_get($this->query()->all(), $key); + + return $this->withQuery([$key => match (true) { + is_array($currentValue) && array_is_list($currentValue) => array_values(array_unique([...$currentValue, $value])), + is_array($currentValue) => [...$currentValue, $value], + ! is_null($currentValue) => [$currentValue, $value], + default => Arr::wrap($value), + }]); + } - return new static($this->uri->withQuery(Arr::query($newQuery))); + /** + * Remove the given query parameters from the URI. + */ + public function withoutQuery(array $keys): static + { + return $this->replaceQuery(Arr::except($this->query()->all(), $keys)); } /** * Specify new query parameters for the URI. */ - public function replaceQuery(Stringable|array|string $query) + public function replaceQuery(array $query): static { return $this->withQuery($query, merge: false); } From 7ef5839b9a4354fac42fd04fd66fabd169a217d9 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 10:27:42 -0800 Subject: [PATCH 05/15] working on uris --- src/Illuminate/Support/Uri.php | 51 +++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index 7f0d4af22bcf..14e03feee20a 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -5,6 +5,8 @@ use Closure; use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Contracts\Support\Responsable; +use Illuminate\Http\RedirectResponse; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Tappable; use League\Uri\Contracts\UriInterface; @@ -12,7 +14,7 @@ use SensitiveParameter; use Stringable; -class Uri implements Htmlable +class Uri implements Htmlable, Responsable { use Conditionable, Tappable; @@ -264,19 +266,22 @@ public function withFragment(string $fragment): static } /** - * Set the URL generator resolver. + * Create a redirect HTTP response for the given URI. */ - public static function setUrlGeneratorResolver(Closure $urlGeneratorResolver): void + public function redirect(int $status = 302, array $headers = []): RedirectResponse { - static::$urlGeneratorResolver = $urlGeneratorResolver; + return new RedirectResponse((string) $this, $status, $headers); } /** - * Get the underlying URI instance. + * Create an HTTP response that represents the object. + * + * @param \Illuminate\Http\Request $request + * @return \Symfony\Component\HttpFoundation\Response */ - public function getUri(): UriInterface + public function toResponse($request) { - return $this->uri; + return new RedirectResponse((string) $this); } /** @@ -289,6 +294,38 @@ public function toHtml() return (string) $this; } + /** + * Get the string representation of the URI. + */ + public function value(): string + { + return (string) $this; + } + + /** + * Determine if the URI is currently an empty string. + */ + public function isEmpty(): bool + { + return trim((string) $this) === ''; + } + + /** + * Set the URL generator resolver. + */ + public static function setUrlGeneratorResolver(Closure $urlGeneratorResolver): void + { + static::$urlGeneratorResolver = $urlGeneratorResolver; + } + + /** + * Get the underlying URI instance. + */ + public function getUri(): UriInterface + { + return $this->uri; + } + /** * Get the string representation of the URI. */ From 9694d3019c87557dec006bf62406d6412da888a8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 12:31:29 -0800 Subject: [PATCH 06/15] update composer files --- composer.json | 1 + src/Illuminate/Support/composer.json | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ee8ee45920c5..cbc9f6bba34c 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "league/commonmark": "^2.2.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", + "league/uri": "^7.4", "monolog/monolog": "^3.0", "nesbot/carbon": "^2.72.2|^3.4", "nunomaduro/termwind": "^2.0", diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 986309901b08..3c208bcacf84 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -47,11 +47,12 @@ } }, "suggest": { - "illuminate/filesystem": "Required to use the composer class (^11.0).", + "illuminate/filesystem": "Required to use the Composer class (^11.0).", "laravel/serializable-closure": "Required to use the once function (^1.3).", "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", + "league/uri": "Required to use the Uri class (^7.4).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", - "symfony/process": "Required to use the composer class (^7.0).", + "symfony/process": "Required to use the Composer class (^7.0).", "symfony/uid": "Required to use Str::ulid() (^7.0).", "symfony/var-dumper": "Required to use the dd function (^7.0).", "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." From bb339c1ed0f88d5ce57a59cdd86f4535a283cecb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 12:37:31 -0800 Subject: [PATCH 07/15] tweak --- src/Illuminate/Support/Uri.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index 14e03feee20a..3ca7642163c5 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -233,11 +233,13 @@ public function pushOntoQuery(string $key, mixed $value): static { $currentValue = data_get($this->query()->all(), $key); + $values = Arr::wrap($value); + return $this->withQuery([$key => match (true) { - is_array($currentValue) && array_is_list($currentValue) => array_values(array_unique([...$currentValue, $value])), - is_array($currentValue) => [...$currentValue, $value], - ! is_null($currentValue) => [$currentValue, $value], - default => Arr::wrap($value), + is_array($currentValue) && array_is_list($currentValue) => array_values(array_unique([...$currentValue, ...$values])), + is_array($currentValue) => [...$currentValue, ...$values], + ! is_null($currentValue) => [$currentValue, ...$values], + default => $values, }]); } From 0f3ad8758352d22b9e0adf4c2a8f6cdba7d00903 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 2 Dec 2024 13:07:59 -0800 Subject: [PATCH 08/15] add tests --- src/Illuminate/Support/UriQueryString.php | 8 ++ tests/Support/SupportUriTest.php | 113 ++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/Support/SupportUriTest.php diff --git a/src/Illuminate/Support/UriQueryString.php b/src/Illuminate/Support/UriQueryString.php index 1dd0b697a89c..a5037466678c 100644 --- a/src/Illuminate/Support/UriQueryString.php +++ b/src/Illuminate/Support/UriQueryString.php @@ -68,6 +68,14 @@ public function decode(): string return rawurldecode((string) $this); } + /** + * Get the string representation of the query string. + */ + public function value(): string + { + return (string) $this; + } + /** * Convert the query string into an array. */ diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php new file mode 100644 index 000000000000..30b8771672e4 --- /dev/null +++ b/tests/Support/SupportUriTest.php @@ -0,0 +1,113 @@ +assertEquals('https', $uri->scheme()); + $this->assertNull($uri->user()); + $this->assertNull($uri->password()); + $this->assertEquals('laravel.com', $uri->host()); + $this->assertNull($uri->port()); + $this->assertEquals('docs/installation', $uri->path()); + $this->assertEquals([], $uri->query()->toArray()); + $this->assertEquals('', (string) $uri->query()); + $this->assertEquals('', $uri->query()->decode()); + $this->assertNull($uri->fragment()); + $this->assertEquals($originalUri, (string) $uri); + + $uri = Uri::of('https://taylor:password@laravel.com/docs/installation?version=1#hello'); + + $this->assertEquals('taylor', $uri->user()); + $this->assertEquals('password', $uri->password()); + $this->assertEquals('hello', $uri->fragment()); + $this->assertEquals(['version' => 1], $uri->query()->all()); + $this->assertEquals(1, $uri->query()->integer('version')); + } + + public function test_complicated_query_string_parsing() + { + $uri = Uri::of('https://example.com/users?key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&flag_value'); + + $this->assertEquals([ + 'key_1' => 'value', + 'key_2' => [ + 'sub_field' => 'value', + ], + 'key_3' => [ + 'value', + ], + 'key_4' => [ + 9 => 'value', + ], + 'key_5' => [ + [ + [ + 'foo' => [ + 9 => 'bar', + ], + ] + ], + ], + 'flag_value' => '', + ], $uri->query()->all()); + + $this->assertEquals('key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&flag_value', $uri->query()->decode()); + } + + public function test_uri_building() + { + $uri = Uri::of(); + + $uri = $uri->withHost('laravel.com') + ->withScheme('https') + ->withUser('taylor', 'password') + ->withPath('/docs/installation') + ->withPort(80) + ->withQuery(['version' => 1]) + ->withFragment('hello'); + + $this->assertEquals('https://taylor:password@laravel.com:80/docs/installation?version=1#hello', (string) $uri); + } + + public function test_complicated_query_string_manipulation() + { + $uri = Uri::of('https://laravel.com'); + + $uri = $uri->withQuery([ + 'name' => 'Taylor', + 'age' => 38, + 'role' => [ + 'title' => 'Developer', + 'focus' => 'PHP', + ], + 'tags' => [ + 'person', + 'employee', + ], + 'flag' => '', + ])->withoutQuery(['name']); + + $this->assertEquals('age=38&role[title]=Developer&role[focus]=PHP&tags[0]=person&tags[1]=employee&flag=', $uri->query()->decode()); + $this->assertEquals('name=Taylor', $uri->replaceQuery(['name' => 'Taylor'])->query()->decode()); + + // Push onto multi-value and missing items... + $uri = Uri::of('https://laravel.com?tags[]=foo'); + + $this->assertEquals(['tags' => ['foo', 'bar']], $uri->pushOntoQuery('tags', 'bar')->query()->all()); + $this->assertEquals(['tags' => ['foo', 'bar', 'baz']], $uri->pushOntoQuery('tags', ['bar', 'baz'])->query()->all()); + $this->assertEquals(['tags' => ['foo'], 'names' => ['Taylor']], $uri->pushOntoQuery('names', 'Taylor')->query()->all()); + + // Push onto single value item... + $uri = Uri::of('https://laravel.com?tag=foo'); + + $this->assertEquals(['tag' => ['foo', 'bar']], $uri->pushOntoQuery('tag', 'bar')->query()->all()); + } +} From 90303e225b4a665f2765df46f6a1201d95c1102f Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 2 Dec 2024 21:28:04 +0000 Subject: [PATCH 09/15] Apply fixes from StyleCI --- tests/Support/SupportUriTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 30b8771672e4..89e017c75fcc 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -53,7 +53,7 @@ public function test_complicated_query_string_parsing() 'foo' => [ 9 => 'bar', ], - ] + ], ], ], 'flag_value' => '', From 69051ff86d28a113ed3a358cde421a0abf3eb613 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 3 Dec 2024 09:30:30 -0800 Subject: [PATCH 10/15] Update src/Illuminate/Support/UriQueryString.php Co-authored-by: Mior Muhammad Zaki --- src/Illuminate/Support/UriQueryString.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/UriQueryString.php b/src/Illuminate/Support/UriQueryString.php index a5037466678c..87fbb2eb80e8 100644 --- a/src/Illuminate/Support/UriQueryString.php +++ b/src/Illuminate/Support/UriQueryString.php @@ -12,9 +12,9 @@ class UriQueryString implements Arrayable /** * Create a new URI query string instance. */ - public function __construct(Uri $uri) + public function __construct(protected Uri $uri) { - $this->uri = $uri; + // } /** From 7b923983c0b22f950659d6f7936c3b9096a51b87 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 3 Dec 2024 09:31:15 -0800 Subject: [PATCH 11/15] Update src/Illuminate/Support/UriQueryString.php Co-authored-by: Hafez Divandari --- src/Illuminate/Support/UriQueryString.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/UriQueryString.php b/src/Illuminate/Support/UriQueryString.php index 87fbb2eb80e8..34abb0a37cf3 100644 --- a/src/Illuminate/Support/UriQueryString.php +++ b/src/Illuminate/Support/UriQueryString.php @@ -81,7 +81,11 @@ public function value(): string */ public function toArray() { - parse_str($this->uri->getUri()->getQuery(), $currentQuery); + if (is_null($query = $this->uri->getUri()->getQuery())) { + return []; + } + + parse_str($query, $currentQuery); return $currentQuery; } From 51233af0599faaac400dcf5baa4e78bd80147a4a Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 8 Dec 2024 18:46:12 +0330 Subject: [PATCH 12/15] [URI] Enhance URI class (#53796) * enhance URI class * Update Uri.php --------- Co-authored-by: Taylor Otwell --- composer.json | 2 +- src/Illuminate/Support/Uri.php | 33 +++++++++-------------- src/Illuminate/Support/UriQueryString.php | 9 ++----- src/Illuminate/Support/composer.json | 2 +- tests/Support/SupportUriTest.php | 5 ++-- 5 files changed, 20 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index cbc9f6bba34c..f8456945415f 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "league/commonmark": "^2.2.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", - "league/uri": "^7.4", + "league/uri": "^7.5.1", "monolog/monolog": "^3.0", "nesbot/carbon": "^2.72.2|^3.4", "nunomaduro/termwind": "^2.0", diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index 3ca7642163c5..d4b0deb42e77 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -18,6 +18,11 @@ class Uri implements Htmlable, Responsable { use Conditionable, Tappable; + /** + * The URI instance. + */ + protected UriInterface $uri; + /** * The URL generator resolver. */ @@ -75,19 +80,9 @@ public function scheme(): ?string */ public function user(bool $withPassword = false): ?string { - if ($withPassword) { - return $this->uri->getUserInfo(); - } - - $userInfo = $this->uri->getUserInfo(); - - if (is_null($userInfo)) { - return null; - } - - return str_contains($userInfo, ':') - ? Str::before($userInfo, ':') - : $userInfo; + return $withPassword + ? $this->uri->getUserInfo() + : $this->uri->getUsername(); } /** @@ -95,9 +90,7 @@ public function user(bool $withPassword = false): ?string */ public function password(): ?string { - $userInfo = $this->uri->getUserInfo(); - - return ! is_null($userInfo) ? Str::after($userInfo, ':') : null; + return $this->uri->getPassword(); } /** @@ -272,7 +265,7 @@ public function withFragment(string $fragment): static */ public function redirect(int $status = 302, array $headers = []): RedirectResponse { - return new RedirectResponse((string) $this, $status, $headers); + return new RedirectResponse($this->value(), $status, $headers); } /** @@ -283,7 +276,7 @@ public function redirect(int $status = 302, array $headers = []): RedirectRespon */ public function toResponse($request) { - return new RedirectResponse((string) $this); + return new RedirectResponse($this->value()); } /** @@ -293,7 +286,7 @@ public function toResponse($request) */ public function toHtml() { - return (string) $this; + return $this->value(); } /** @@ -309,7 +302,7 @@ public function value(): string */ public function isEmpty(): bool { - return trim((string) $this) === ''; + return trim($this->value()) === ''; } /** diff --git a/src/Illuminate/Support/UriQueryString.php b/src/Illuminate/Support/UriQueryString.php index 34abb0a37cf3..a9ae5cead397 100644 --- a/src/Illuminate/Support/UriQueryString.php +++ b/src/Illuminate/Support/UriQueryString.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Traits\InteractsWithData; +use League\Uri\QueryString; class UriQueryString implements Arrayable { @@ -81,13 +82,7 @@ public function value(): string */ public function toArray() { - if (is_null($query = $this->uri->getUri()->getQuery())) { - return []; - } - - parse_str($query, $currentQuery); - - return $currentQuery; + return QueryString::extract($this->value()); } /** diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 3c208bcacf84..1c2a052136bf 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -50,7 +50,7 @@ "illuminate/filesystem": "Required to use the Composer class (^11.0).", "laravel/serializable-closure": "Required to use the once function (^1.3).", "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", - "league/uri": "Required to use the Uri class (^7.4).", + "league/uri": "Required to use the Uri class (^7.5.1).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", "symfony/process": "Required to use the Composer class (^7.0).", "symfony/uid": "Required to use Str::ulid() (^7.0).", diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 89e017c75fcc..05e916aec3fc 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -34,7 +34,7 @@ public function test_basic_uri_interactions() public function test_complicated_query_string_parsing() { - $uri = Uri::of('https://example.com/users?key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&flag_value'); + $uri = Uri::of('https://example.com/users?key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value'); $this->assertEquals([ 'key_1' => 'value', @@ -56,10 +56,11 @@ public function test_complicated_query_string_parsing() ], ], ], + 'key.6' => 'value', 'flag_value' => '', ], $uri->query()->all()); - $this->assertEquals('key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&flag_value', $uri->query()->decode()); + $this->assertEquals('key_1=value&key_2[sub_field]=value&key_3[]=value&key_4[9]=value&key_5[][][foo][9]=bar&key.6=value&flag_value', $uri->query()->decode()); } public function test_uri_building() From 462b359c2af9ba4d7afb7389499842e5ed9083eb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 9 Dec 2024 09:50:47 -0600 Subject: [PATCH 13/15] make dot behavior consistent on replaceQuery and withQuery --- src/Illuminate/Support/Uri.php | 6 +++++- tests/Support/SupportUriTest.php | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index d4b0deb42e77..a0ec47d0f748 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -197,7 +197,11 @@ public function withQuery(array $query, bool $merge = true): static $newQuery = $mergedQuery; } else { - $newQuery = $query; + $newQuery = []; + + foreach ($query as $key => $value) { + data_set($newQuery, $key, $value); + } } return new static($this->uri->withQuery(Arr::query($newQuery))); diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 05e916aec3fc..780f50580f69 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -111,4 +111,12 @@ public function test_complicated_query_string_manipulation() $this->assertEquals(['tag' => ['foo', 'bar']], $uri->pushOntoQuery('tag', 'bar')->query()->all()); } + + public function test_query_strings_with_dots_can_be_replaced_or_merged_consistently() + { + $uri = Uri::of('https://dot.test/?foo.bar=baz'); + + $this->assertEquals('foo.bar=baz&foo[bar]=zab', $uri->withQuery(['foo.bar' => 'zab'])->query()->decode()); + $this->assertEquals('foo[bar]=zab', $uri->replaceQuery(['foo.bar' => 'zab'])->query()->decode()); + } } From c0e0177f6737387ec9ddf7ea5f062417b4df82b0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 9 Dec 2024 10:00:26 -0600 Subject: [PATCH 14/15] add decode method to uri --- src/Illuminate/Support/Uri.php | 13 +++++++++++++ tests/Support/SupportUriTest.php | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index a0ec47d0f748..bda8ede6831a 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Str; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Tappable; use League\Uri\Contracts\UriInterface; @@ -293,6 +294,18 @@ public function toHtml() return $this->value(); } + /** + * Get the decoded string representation of the URI. + */ + public function decode(): string + { + if (empty($this->query()->toArray())) { + return $this->value(); + } + + return Str::replace(Str::after($this->value(), '?'), $this->query()->decode(), $this->value()); + } + /** * Get the string representation of the URI. */ diff --git a/tests/Support/SupportUriTest.php b/tests/Support/SupportUriTest.php index 780f50580f69..95e05ea8791e 100644 --- a/tests/Support/SupportUriTest.php +++ b/tests/Support/SupportUriTest.php @@ -119,4 +119,11 @@ public function test_query_strings_with_dots_can_be_replaced_or_merged_consisten $this->assertEquals('foo.bar=baz&foo[bar]=zab', $uri->withQuery(['foo.bar' => 'zab'])->query()->decode()); $this->assertEquals('foo[bar]=zab', $uri->replaceQuery(['foo.bar' => 'zab'])->query()->decode()); } + + public function test_decoding_the_entire_uri() + { + $uri = Uri::of('https://laravel.com/docs/11.x/installation')->withQuery(['tags' => ['first', 'second']]); + + $this->assertEquals('https://laravel.com/docs/11.x/installation?tags[0]=first&tags[1]=second', $uri->decode()); + } } From d22864d983b432251853ba2cc2cd387340f878c4 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Mon, 9 Dec 2024 16:00:49 +0000 Subject: [PATCH 15/15] Apply fixes from StyleCI --- src/Illuminate/Support/Uri.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Support/Uri.php b/src/Illuminate/Support/Uri.php index bda8ede6831a..fbc524b1796d 100644 --- a/src/Illuminate/Support/Uri.php +++ b/src/Illuminate/Support/Uri.php @@ -7,7 +7,6 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Http\RedirectResponse; -use Illuminate\Support\Str; use Illuminate\Support\Traits\Conditionable; use Illuminate\Support\Traits\Tappable; use League\Uri\Contracts\UriInterface;