Operator.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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\UriTemplate;
  12. use League\Uri\Encoder;
  13. use League\Uri\Exceptions\SyntaxError;
  14. use Stringable;
  15. use function implode;
  16. use function is_array;
  17. use function preg_match;
  18. use function rawurlencode;
  19. use function str_contains;
  20. use function substr;
  21. /**
  22. * Processing behavior according to the expression type operator.
  23. *
  24. * @internal The class exposes the internal representation of an Operator and its usage
  25. *
  26. * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2
  27. * @link https://tools.ietf.org/html/rfc6570#appendix-A
  28. */
  29. enum Operator: string
  30. {
  31. /**
  32. * Expression regular expression pattern.
  33. *
  34. * @link https://tools.ietf.org/html/rfc6570#section-2.2
  35. */
  36. private const REGEXP_EXPRESSION = '/^\{(?:(?<operator>[\.\/;\?&\=,\!@\|\+#])?(?<variables>[^\}]*))\}$/';
  37. /**
  38. * Reserved Operator characters.
  39. *
  40. * @link https://tools.ietf.org/html/rfc6570#section-2.2
  41. */
  42. private const RESERVED_OPERATOR = '=,!@|';
  43. case None = '';
  44. case ReservedChars = '+';
  45. case Label = '.';
  46. case Path = '/';
  47. case PathParam = ';';
  48. case Query = '?';
  49. case QueryPair = '&';
  50. case Fragment = '#';
  51. public function first(): string
  52. {
  53. return match ($this) {
  54. self::None, self::ReservedChars => '',
  55. default => $this->value,
  56. };
  57. }
  58. public function separator(): string
  59. {
  60. return match ($this) {
  61. self::None, self::ReservedChars, self::Fragment => ',',
  62. self::Query, self::QueryPair => '&',
  63. default => $this->value,
  64. };
  65. }
  66. public function isNamed(): bool
  67. {
  68. return match ($this) {
  69. self::Query, self::PathParam, self::QueryPair => true,
  70. default => false,
  71. };
  72. }
  73. /**
  74. * Removes percent encoding on reserved characters (used with + and # modifiers).
  75. */
  76. public function decode(string $var): string
  77. {
  78. return match ($this) {
  79. Operator::ReservedChars, Operator::Fragment => (string) Encoder::encodeQueryOrFragment($var),
  80. default => rawurlencode($var),
  81. };
  82. }
  83. /**
  84. * @throws SyntaxError if the expression is invalid
  85. * @throws SyntaxError if the operator used in the expression is invalid
  86. * @throws SyntaxError if the contained variable specifiers are invalid
  87. *
  88. * @return array{operator:Operator, variables:string}
  89. */
  90. public static function parseExpression(Stringable|string $expression): array
  91. {
  92. $expression = (string) $expression;
  93. if (1 !== preg_match(self::REGEXP_EXPRESSION, $expression, $parts)) {
  94. throw new SyntaxError('The expression "'.$expression.'" is invalid.');
  95. }
  96. /** @var array{operator:string, variables:string} $parts */
  97. $parts = $parts + ['operator' => ''];
  98. if ('' !== $parts['operator'] && str_contains(self::RESERVED_OPERATOR, $parts['operator'])) {
  99. throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.');
  100. }
  101. return [
  102. 'operator' => self::from($parts['operator']),
  103. 'variables' => $parts['variables'],
  104. ];
  105. }
  106. /**
  107. * Replaces an expression with the given variables.
  108. *
  109. * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
  110. * @throws TemplateCanNotBeExpanded if the variables contains nested array values
  111. */
  112. public function expand(VarSpecifier $varSpecifier, VariableBag $variables): string
  113. {
  114. $value = $variables->fetch($varSpecifier->name);
  115. if (null === $value) {
  116. return '';
  117. }
  118. [$expanded, $actualQuery] = $this->inject($value, $varSpecifier);
  119. if (!$actualQuery) {
  120. return $expanded;
  121. }
  122. if ('&' !== $this->separator() && '' === $expanded) {
  123. return $varSpecifier->name;
  124. }
  125. return $varSpecifier->name.'='.$expanded;
  126. }
  127. /**
  128. * @param string|array<string> $value
  129. *
  130. * @return array{0:string, 1:bool}
  131. */
  132. private function inject(array|string $value, VarSpecifier $varSpec): array
  133. {
  134. if (is_array($value)) {
  135. return $this->replaceList($value, $varSpec);
  136. }
  137. if (':' === $varSpec->modifier) {
  138. $value = substr($value, 0, $varSpec->position);
  139. }
  140. return [$this->decode($value), $this->isNamed()];
  141. }
  142. /**
  143. * Expands an expression using a list of values.
  144. *
  145. * @param array<string> $value
  146. *
  147. * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
  148. *
  149. * @return array{0:string, 1:bool}
  150. */
  151. private function replaceList(array $value, VarSpecifier $varSpec): array
  152. {
  153. if (':' === $varSpec->modifier) {
  154. throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name);
  155. }
  156. if ([] === $value) {
  157. return ['', false];
  158. }
  159. $pairs = [];
  160. $isList = array_is_list($value);
  161. $useQuery = $this->isNamed();
  162. foreach ($value as $key => $var) {
  163. if (!$isList) {
  164. $key = rawurlencode((string) $key);
  165. }
  166. $var = $this->decode($var);
  167. if ('*' === $varSpec->modifier) {
  168. if (!$isList) {
  169. $var = $key.'='.$var;
  170. } elseif ($key > 0 && $useQuery) {
  171. $var = $varSpec->name.'='.$var;
  172. }
  173. }
  174. $pairs[$key] = $var;
  175. }
  176. if ('*' === $varSpec->modifier) {
  177. if (!$isList) {
  178. // Don't prepend the value name when using the `explode` modifier with an associative array.
  179. $useQuery = false;
  180. }
  181. return [implode($this->separator(), $pairs), $useQuery];
  182. }
  183. if (!$isList) {
  184. // When an associative array is encountered and the `explode` modifier is not set, then
  185. // the result must be a comma separated list of keys followed by their respective values.
  186. $retVal = [];
  187. foreach ($pairs as $offset => $data) {
  188. $retVal[$offset] = $offset.','.$data;
  189. }
  190. $pairs = $retVal;
  191. }
  192. return [implode(',', $pairs), $useQuery];
  193. }
  194. }