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);