UriTemplate.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. /**
  3. * League.Uri (https://uri.thephpleague.com)
  4. *
  5. * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace League\Uri;
  12. use BackedEnum;
  13. use Deprecated;
  14. use League\Uri\Contracts\UriException;
  15. use League\Uri\Contracts\UriInterface;
  16. use League\Uri\Exceptions\MissingFeature;
  17. use League\Uri\Exceptions\SyntaxError;
  18. use League\Uri\UriTemplate\Template;
  19. use League\Uri\UriTemplate\TemplateCanNotBeExpanded;
  20. use League\Uri\UriTemplate\VariableBag;
  21. use Psr\Http\Message\UriFactoryInterface;
  22. use Psr\Http\Message\UriInterface as Psr7UriInterface;
  23. use Stringable;
  24. use Uri\InvalidUriException;
  25. use Uri\Rfc3986\Uri as Rfc3986Uri;
  26. use Uri\WhatWg\InvalidUrlException;
  27. use Uri\WhatWg\Url as WhatWgUrl;
  28. use function array_fill_keys;
  29. use function array_key_exists;
  30. use function class_exists;
  31. /**
  32. * Defines the URI Template syntax and the process for expanding a URI Template into a URI reference.
  33. *
  34. * @link https://tools.ietf.org/html/rfc6570
  35. * @package League\Uri
  36. * @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
  37. * @since 6.1.0
  38. *
  39. * @phpstan-import-type InputValue from VariableBag
  40. */
  41. final class UriTemplate implements Stringable
  42. {
  43. private readonly Template $template;
  44. private readonly VariableBag $defaultVariables;
  45. /**
  46. * @throws SyntaxError if the template syntax is invalid
  47. * @throws TemplateCanNotBeExpanded if the template or the variables are invalid
  48. */
  49. public function __construct(BackedEnum|Stringable|string $template, iterable $defaultVariables = [])
  50. {
  51. $this->template = $template instanceof Template ? $template : Template::new($template);
  52. $this->defaultVariables = $this->filterVariables($defaultVariables);
  53. }
  54. private function filterVariables(iterable $variables): VariableBag
  55. {
  56. if (!$variables instanceof VariableBag) {
  57. $variables = new VariableBag($variables);
  58. }
  59. return $variables
  60. ->filter(fn ($value, string|int $name) => array_key_exists(
  61. $name,
  62. array_fill_keys($this->template->variableNames, 1)
  63. ));
  64. }
  65. /**
  66. * Returns the string representation of the UriTemplate.
  67. */
  68. public function __toString(): string
  69. {
  70. return $this->template->value;
  71. }
  72. /**
  73. * Returns the distinct variables placeholders used in the template.
  74. *
  75. * @return array<string>
  76. */
  77. public function getVariableNames(): array
  78. {
  79. return $this->template->variableNames;
  80. }
  81. /**
  82. * @return array<string, InputValue>
  83. */
  84. public function getDefaultVariables(): array
  85. {
  86. return iterator_to_array($this->defaultVariables);
  87. }
  88. /**
  89. * Returns a new instance with the updated default variables.
  90. *
  91. * This method MUST retain the state of the current instance, and return
  92. * an instance that contains the modified default variables.
  93. *
  94. * If present, variables whose name is not part of the current template
  95. * possible variable names are removed.
  96. *
  97. * @throws TemplateCanNotBeExpanded if the variables are invalid
  98. */
  99. public function withDefaultVariables(iterable $defaultVariables): self
  100. {
  101. $defaultVariables = $this->filterVariables($defaultVariables);
  102. if ($this->defaultVariables->equals($defaultVariables)) {
  103. return $this;
  104. }
  105. return new self($this->template, $defaultVariables);
  106. }
  107. private function templateExpanded(iterable $variables = []): string
  108. {
  109. return $this->template->expand($this->filterVariables($variables)->replace($this->defaultVariables));
  110. }
  111. private function templateExpandedOrFail(iterable $variables = []): string
  112. {
  113. return $this->template->expandOrFail($this->filterVariables($variables)->replace($this->defaultVariables));
  114. }
  115. /**
  116. * @throws TemplateCanNotBeExpanded if the variables are invalid
  117. * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
  118. */
  119. public function expand(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface
  120. {
  121. $expanded = $this->templateExpanded($variables);
  122. return null === $baseUri ? Uri::new($expanded) : (Uri::parse($expanded, $baseUri) ?? throw new SyntaxError('Unable to expand URI'));
  123. }
  124. /**
  125. * @throws MissingFeature if no Uri\Rfc3986\Uri class is found
  126. * @throws TemplateCanNotBeExpanded if the variables are invalid
  127. * @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance
  128. * @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance
  129. */
  130. public function expandToUri(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri
  131. {
  132. class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
  133. return new Rfc3986Uri($this->templateExpanded($variables), $this->newRfc3986Uri($baseUri));
  134. }
  135. /**
  136. * @throws MissingFeature if no Uri\Whatwg\Url class is found
  137. * @throws TemplateCanNotBeExpanded if the variables are invalid
  138. * @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
  139. * @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
  140. */
  141. public function expandToUrl(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
  142. {
  143. class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
  144. return new WhatWgUrl($this->templateExpanded($variables), $this->newWhatWgUrl($baseUrl), $errors);
  145. }
  146. /**
  147. * @throws TemplateCanNotBeExpanded if the variables are invalid
  148. * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
  149. */
  150. public function expandToPsr7Uri(
  151. iterable $variables = [],
  152. Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null,
  153. UriFactoryInterface $uriFactory = new HttpFactory()
  154. ): Psr7UriInterface {
  155. $uriString = $this->templateExpandedOrFail($variables);
  156. return $uriFactory->createUri(
  157. null === $baseUrl
  158. ? $uriString
  159. : UriString::resolve($uriString, match (true) {
  160. $baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(),
  161. $baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(),
  162. default => $baseUrl,
  163. })
  164. );
  165. }
  166. /**
  167. * @throws TemplateCanNotBeExpanded if the variables are invalid or missing
  168. * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
  169. */
  170. public function expandOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): UriInterface
  171. {
  172. $expanded = $this->templateExpandedOrFail($variables);
  173. return null === $baseUri ? Uri::new($expanded) : (Uri::parse($expanded, $baseUri) ?? throw new SyntaxError('Unable to expand URI'));
  174. }
  175. /**
  176. * @throws MissingFeature if no Uri\Rfc3986\Uri class is found
  177. * @throws TemplateCanNotBeExpanded if the variables are invalid
  178. * @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance
  179. * @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance
  180. */
  181. public function expandToUriOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Rfc3986Uri
  182. {
  183. class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
  184. return new Rfc3986Uri($this->templateExpandedOrFail($variables), $this->newRfc3986Uri($baseUri));
  185. }
  186. /**
  187. * @throws MissingFeature if no Uri\Whatwg\Url class is found
  188. * @throws TemplateCanNotBeExpanded if the variables are invalid
  189. * @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
  190. * @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
  191. */
  192. public function expandToUrlOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
  193. {
  194. class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
  195. return new WhatWgUrl($this->templateExpandedOrFail($variables), $this->newWhatWgUrl($baseUrl), $errors);
  196. }
  197. /**
  198. * @throws TemplateCanNotBeExpanded if the variables are invalid
  199. * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
  200. */
  201. public function expandToPsr7UriOrFail(
  202. iterable $variables = [],
  203. Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUrl = null,
  204. UriFactoryInterface $uriFactory = new HttpFactory()
  205. ): Psr7UriInterface {
  206. $uriString = $this->templateExpandedOrFail($variables);
  207. return $uriFactory->createUri(
  208. null === $baseUrl
  209. ? $uriString
  210. : UriString::resolve($uriString, match (true) {
  211. $baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(),
  212. $baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(),
  213. default => $baseUrl,
  214. })
  215. );
  216. }
  217. /**
  218. * @throws InvalidUrlException
  219. */
  220. private function newWhatWgUrl(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $url = null): ?WhatWgUrl
  221. {
  222. return match (true) {
  223. null === $url => null,
  224. $url instanceof WhatWgUrl => $url,
  225. $url instanceof Rfc3986Uri => new WhatWgUrl($url->toRawString()),
  226. $url instanceof BackedEnum => new WhatWgUrl((string) $url->value),
  227. default => new WhatWgUrl((string) $url),
  228. };
  229. }
  230. /**
  231. * @throws InvalidUriException
  232. */
  233. private function newRfc3986Uri(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $uri = null): ?Rfc3986Uri
  234. {
  235. return match (true) {
  236. null === $uri => null,
  237. $uri instanceof Rfc3986Uri => $uri,
  238. $uri instanceof WhatWgUrl => new Rfc3986Uri($uri->toAsciiString()),
  239. $uri instanceof BackedEnum => new Rfc3986Uri((string) $uri->value),
  240. default => new Rfc3986Uri((string) $uri),
  241. };
  242. }
  243. /**
  244. * DEPRECATION WARNING! This method will be removed in the next major point release.
  245. *
  246. * @deprecated Since version 7.6.0
  247. * @codeCoverageIgnore
  248. * @see UriTemplate::toString()
  249. *
  250. * Create a new instance from the environment.
  251. */
  252. #[Deprecated(message:'use League\Uri\UriTemplate::__toString() instead', since:'league/uri:7.6.0')]
  253. public function getTemplate(): string
  254. {
  255. return $this->__toString();
  256. }
  257. }