From 432904515ecc68c8e3d52aef4fbf719dd2ab2b19 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 08:43:47 +0200 Subject: [PATCH] Upstream thephpleague/uri-src state of the touched files before dc0df9e387 --- docs/uri/7.0/uri-template.md | 25 ---------- uri/UriTemplate/Template.php | 84 +++++++++----------------------- uri/UriTemplate/TemplateTest.php | 57 ---------------------- 3 files changed, 23 insertions(+), 143 deletions(-) diff --git a/docs/uri/7.0/uri-template.md b/docs/uri/7.0/uri-template.md index 4951eeb..5d21072 100644 --- a/docs/uri/7.0/uri-template.md +++ b/docs/uri/7.0/uri-template.md @@ -85,31 +85,6 @@ echo $uriTemplate->expandOrFail($params);

expandToUriOrFail() and expandToUrlOrFail() are available since version 7.6.0

-### Literal Expansion - -The characters located outside of the template expressions are literals. As per -[RFC6570 section 3.1](https://www.rfc-editor.org/rfc/rfc6570#section-3.1) a literal character is -copied as is when it is allowed anywhere in the URI syntax, in other words when it belongs to the -`unreserved / reserved / pct-encoded` set. Any other character is percent encoded using its UTF-8 -octets. - -~~~php -$uriTemplate = new UriTemplate('https://example.com/café/{hotel}'); - -echo $uriTemplate->expand(['hotel' => 'Rest & Relax']), PHP_EOL; -// display https://example.com/caf%C3%A9/Rest%20%26%20Relax -~~~ - -Percent encoded triplets already present in the template are left untouched, they are never -encoded a second time. - -~~~php -$uriTemplate = new UriTemplate('https://example.com/caf%C3%A9/{hotel}'); - -echo $uriTemplate->expand(['hotel' => 'Rest & Relax']), PHP_EOL; -// display https://example.com/caf%C3%A9/Rest%20%26%20Relax -~~~ - ## Template Variables

For maximum interoperability you should make sure your variables are diff --git a/uri/UriTemplate/Template.php b/uri/UriTemplate/Template.php index e6314c0..6e0d459 100644 --- a/uri/UriTemplate/Template.php +++ b/uri/UriTemplate/Template.php @@ -20,18 +20,14 @@ use Stringable; use function array_filter; use function array_map; +use function array_reduce; use function array_unique; -use function array_values; -use function implode; +use function preg_match_all; use function preg_replace; -use function preg_replace_callback; -use function preg_split; -use function rawurlencode; -use function str_starts_with; +use function str_replace; use function strpbrk; -use const PREG_SPLIT_DELIM_CAPTURE; -use const PREG_SPLIT_NO_EMPTY; +use const PREG_SET_ORDER; /** * @internal The class exposes the internal representation of a Template and its usage @@ -43,36 +39,19 @@ final class Template implements Stringable */ private const REGEXP_EXPRESSION_DETECTOR = '/(?\{[^}]*})/x'; - /** - * Literal characters which must be percent encoded in the expanded - * template: everything outside "unreserved / reserved / pct-encoded", a - * "%" starting no triplet included. - * - * @link https://www.rfc-editor.org/rfc/rfc6570#section-3.1 - */ - private const REGEXP_LITERAL_TO_ENCODE = '/[^A-Za-z\d\-._~:\/?#\[\]@!$&\'()*+,;=%]+|%(?![A-Fa-f\d]{2})/'; - - /** @var array */ - private readonly array $parts; + /** @var array */ + private readonly array $expressions; /** @var array */ public readonly array $variableNames; - private function __construct(public readonly string $value, string|Expression ...$parts) + private function __construct(public readonly string $value, Expression ...$expressions) { - $this->parts = $parts; - - $expressions = []; - foreach ($parts as $part) { - if ($part instanceof Expression) { - $expressions[$part->value] = $part; - } - } - + $this->expressions = $expressions; $this->variableNames = array_unique( array_merge( ...array_map( static fn (Expression $expression): array => $expression->variableNames, - array_values($expressions) + $expressions ) ) ); @@ -93,35 +72,17 @@ final class Template implements Stringable $remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template); false === strpbrk($remainder, '{}') || throw new SyntaxError('The template "'.$template.'" contains invalid expressions.'); - /** @var array $segments */ - $segments = preg_split(self::REGEXP_EXPRESSION_DETECTOR, $template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER); - $parts = []; - $expressions = []; - foreach ($segments as $segment) { - if (!str_starts_with($segment, '{')) { - $parts[] = self::encodeLiteral($segment); - continue; - } + return new self($template, ...array_values( + array_reduce($founds, function (array $carry, array $found): array { + if (!isset($carry[$found['expression']])) { + $carry[$found['expression']] = Expression::new($found['expression']); + } - $parts[] = $expressions[$segment] ??= Expression::new($segment); - } - - return new self($template, ...$parts); - } - - /** - * Percent encodes the template characters which are not allowed to be copied as-is into a URI. - * - * @link https://www.rfc-editor.org/rfc/rfc6570#section-3.1 - */ - private static function encodeLiteral(string $literal): string - { - return (string) preg_replace_callback( - self::REGEXP_LITERAL_TO_ENCODE, - static fn (array $matches): string => rawurlencode($matches[0]), - $literal - ); + return $carry; + }, []) + )); } /** @@ -155,10 +116,11 @@ final class Template implements Stringable private function expandAll(VariableBag $variables): string { - return implode('', array_map( - static fn (string|Expression $part): string => $part instanceof Expression ? $part->expand($variables) : $part, - $this->parts - )); + return array_reduce( + $this->expressions, + fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri), + $this->value + ); } public function __toString(): string diff --git a/uri/UriTemplate/TemplateTest.php b/uri/UriTemplate/TemplateTest.php index 28f94c6..85f7dd2 100644 --- a/uri/UriTemplate/TemplateTest.php +++ b/uri/UriTemplate/TemplateTest.php @@ -174,63 +174,6 @@ final class TemplateTest extends TestCase ]; } - #[DataProvider('providesLiteralEncoding')] - #[Test] - public function testItEncodesTheTemplateLiterals(string $notation, array $variables, string $expected): void - { - self::assertSame($expected, Template::new($notation)->expand($variables)); - } - - /** - * @see https://www.rfc-editor.org/rfc/rfc6570#section-3.1 - */ - public static function providesLiteralEncoding(): iterable - { - $variables = [ - 'var' => 'value', - 'path' => '/foo/bar', - 'hello' => 'Hello World!', - ]; - - return [ - 'a character disallowed in a URI is encoded as its UTF-8 octets' => [ - 'notation' => 'café/{var}', - 'variables' => $variables, - 'expected' => 'caf%C3%A9/value', - ], - 'a percent encoded triplet is copied as is' => [ - 'notation' => 'x%20y/{var}', - 'variables' => $variables, - 'expected' => 'x%20y/value', - ], - 'a percent encoded triplet is copied as is on both sides of an expression' => [ - 'notation' => 'x%20y{var}z%20w', - 'variables' => $variables, - 'expected' => 'x%20yvaluez%20w', - ], - 'a space is encoded' => [ - 'notation' => 'a b/{var}', - 'variables' => $variables, - 'expected' => 'a%20b/value', - ], - 'a percent sign which starts no triplet is encoded' => [ - 'notation' => '100%/{var}', - 'variables' => $variables, - 'expected' => '100%25/value', - ], - 'reserved characters are copied as is' => [ - 'notation' => "/a[0]:b@c!d\$e&f'g(h)i*j+k,l;m=n?o#{var}", - 'variables' => $variables, - 'expected' => "/a[0]:b@c!d\$e&f'g(h)i*j+k,l;m=n?o#value", - ], - 'the expanded expressions are never encoded twice' => [ - 'notation' => 'café{+path}{#hello}', - 'variables' => $variables, - 'expected' => 'caf%C3%A9/foo/bar#Hello%20World!', - ], - ]; - } - public function testExpandOrFailIfAtLeastOneVariableIsMissing(): void { $this->expectException(TemplateCanNotBeExpanded::class);