Blame | Letzte Änderung | Log anzeigen | RSS feed
<?phpdeclare(strict_types=1);namespace GuzzleHttp\UriTemplate;/*** Expands URI templates. Userland implementation of PECL uri_template.** @link http://tools.ietf.org/html/rfc6570*/final class UriTemplate{/*** @var array<string, array{prefix:string, joiner:string, query:bool}> Hash for quick operator lookups*/private static $operatorHash = ['' => ['prefix' => '', 'joiner' => ',', 'query' => false],'+' => ['prefix' => '', 'joiner' => ',', 'query' => false],'#' => ['prefix' => '#', 'joiner' => ',', 'query' => false],'.' => ['prefix' => '.', 'joiner' => '.', 'query' => false],'/' => ['prefix' => '/', 'joiner' => '/', 'query' => false],';' => ['prefix' => ';', 'joiner' => ';', 'query' => true],'?' => ['prefix' => '?', 'joiner' => '&', 'query' => true],'&' => ['prefix' => '&', 'joiner' => '&', 'query' => true],];/*** @var string[] Delimiters*/private static $delims = [':','/','?','#','[',']','@','!','$','&','\'','(',')','*','+',',',';','=',];/*** @var string[] Percent encoded delimiters*/private static $delimsPct = ['%3A','%2F','%3F','%23','%5B','%5D','%40','%21','%24','%26','%27','%28','%29','%2A','%2B','%2C','%3B','%3D',];/*** @param array<string,mixed> $variables Variables to use in the template expansion** @throws \RuntimeException*/public static function expand(string $template, array $variables): string{if (false === \strpos($template, '{')) {return $template;}/** @var string|null */$result = \preg_replace_callback('/\{([^\}]+)\}/',self::expandMatchCallback($variables),$template);if (null === $result) {throw new \RuntimeException(\sprintf('Unable to process template: %s', \preg_last_error_msg()));}return $result;}/*** @param array<string,mixed> $variables Variables to use in the template expansion** @return callable(string[]): string*/private static function expandMatchCallback(array $variables): callable{return static function (array $matches) use ($variables): string {return self::expandMatch($matches, $variables);};}/*** Process an expansion** @param array<string,mixed> $variables Variables to use in the template expansion* @param string[] $matches Matches met in the preg_replace_callback** @return string Returns the replacement string*/private static function expandMatch(array $matches, array $variables): string{$replacements = [];$parsed = self::parseExpression($matches[1]);$prefix = self::$operatorHash[$parsed['operator']]['prefix'];$joiner = self::$operatorHash[$parsed['operator']]['joiner'];$useQuery = self::$operatorHash[$parsed['operator']]['query'];$allUndefined = true;foreach ($parsed['values'] as $value) {if (!isset($variables[$value['value']])) {continue;}/** @var mixed */$variable = $variables[$value['value']];$actuallyUseQuery = $useQuery;$expanded = '';if (\is_array($variable)) {$isAssoc = self::isAssoc($variable);$kvp = [];/** @var mixed $var */foreach ($variable as $key => $var) {if ($isAssoc) {$key = \rawurlencode((string) $key);$isNestedArray = \is_array($var);} else {$isNestedArray = false;}if (!$isNestedArray) {$var = \rawurlencode((string) $var);if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {$var = self::decodeReserved($var);}}if ($value['modifier'] === '*') {if ($isAssoc) {if ($isNestedArray) {// Nested arrays must allow for deeply nested structures.$var = \http_build_query([$key => $var], '', '&', \PHP_QUERY_RFC3986);} else {$var = \sprintf('%s=%s', (string) $key, (string) $var);}} elseif ($key > 0 && $actuallyUseQuery) {$var = \sprintf('%s=%s', $value['value'], (string) $var);}}/** @var string $var */$kvp[$key] = $var;}if (0 === \count($variable)) {$actuallyUseQuery = false;} elseif ($value['modifier'] === '*') {$expanded = \implode($joiner, $kvp);if ($isAssoc) {// Don't prepend the value name when using the explode// modifier with an associative array.$actuallyUseQuery = false;}} else {if ($isAssoc) {// When an associative array is encountered and the// explode modifier is not set, then the result must be// a comma separated list of keys followed by their// respective values.foreach ($kvp as $k => &$v) {$v = \sprintf('%s,%s', $k, $v);}}$expanded = \implode(',', $kvp);}} else {$allUndefined = false;if ($value['modifier'] === ':' && isset($value['position'])) {$variable = \substr((string) $variable, 0, $value['position']);}$expanded = \rawurlencode((string) $variable);if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {$expanded = self::decodeReserved($expanded);}}if ($actuallyUseQuery) {if (!$expanded && $joiner !== '&') {$expanded = $value['value'];} else {$expanded = \sprintf('%s=%s', $value['value'], $expanded);}}$replacements[] = $expanded;}$ret = \implode($joiner, $replacements);if ('' === $ret) {// Spec section 3.2.4 and 3.2.5if (false === $allUndefined && ('#' === $prefix || '.' === $prefix)) {return $prefix;}} else {if ('' !== $prefix) {return \sprintf('%s%s', $prefix, $ret);}}return $ret;}/*** Parse an expression into parts** @param string $expression Expression to parse** @return array{operator:string, values:array<array{value:string, modifier:(''|'*'|':'), position?:int}>}*/private static function parseExpression(string $expression): array{$result = [];if (isset(self::$operatorHash[$expression[0]])) {$result['operator'] = $expression[0];/** @var string */$expression = \substr($expression, 1);} else {$result['operator'] = '';}$result['values'] = [];foreach (\explode(',', $expression) as $value) {$value = \trim($value);$varspec = [];if ($colonPos = \strpos($value, ':')) {$varspec['value'] = (string) \substr($value, 0, $colonPos);$varspec['modifier'] = ':';$varspec['position'] = (int) \substr($value, $colonPos + 1);} elseif (\substr($value, -1) === '*') {$varspec['modifier'] = '*';$varspec['value'] = (string) \substr($value, 0, -1);} else {$varspec['value'] = $value;$varspec['modifier'] = '';}$result['values'][] = $varspec;}return $result;}/*** Determines if an array is associative.** This makes the assumption that input arrays are sequences or hashes.* This assumption is a tradeoff for accuracy in favor of speed, but it* should work in almost every case where input is supplied for a URI* template.*/private static function isAssoc(array $array): bool{return $array && \array_keys($array)[0] !== 0;}/*** Removes percent encoding on reserved characters (used with + and #* modifiers).*/private static function decodeReserved(string $string): string{return \str_replace(self::$delimsPct, self::$delims, $string);}}