Файл: vendor/guzzlehttp/uri-template/src/UriTemplate.php
Строк: 274
<?php
declare(strict_types=1);
namespace GuzzleHttpUriTemplate;
/**
* Expands URI templates. Userland implementation of PECL uri_template.
*
* @see https://datatracker.ietf.org/doc/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],
];
/**
* @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'];
$allowReserved = $parsed['operator'] === '+' || $parsed['operator'] === '#';
$hasDefinedVariable = false;
foreach ($parsed['values'] as $value) {
if (!isset($variables[$value['value']])) {
continue;
}
$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) {
$rawKey = (string) $key;
$key = rawurlencode($rawKey);
$isNestedArray = is_array($var);
} else {
$isNestedArray = false;
}
if (!$isNestedArray) {
$var = self::encodeValue(self::stringifyValue($var), $allowReserved);
}
if ($value['modifier'] === '*') {
if ($isAssoc) {
if ($isNestedArray) {
// Nested arrays must allow for deeply nested structures.
$var = http_build_query([$rawKey => self::stringifyNonFiniteFloats($var)], '', '&', PHP_QUERY_RFC3986);
if ($var === '') {
continue;
}
} 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 ($kvp === []) {
continue;
} 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 {
$variable = self::stringifyValue($variable);
if ($value['modifier'] === ':' && isset($value['position'])) {
$variable = substr($variable, 0, $value['position']);
}
$expanded = self::encodeValue($variable, $allowReserved);
}
if ($actuallyUseQuery) {
if ($expanded === '' && $joiner !== '&') {
$expanded = $value['value'];
} else {
$expanded = sprintf('%s=%s', $value['value'], $expanded);
}
}
$hasDefinedVariable = true;
$replacements[] = $expanded;
}
$ret = implode($joiner, $replacements);
// Spec section 3.2.1 and appendix A: the operator's first string is
// appended once any variable in the expression is defined, even when
// every defined value expands to an empty string.
if ('' !== $prefix && $hasDefinedVariable) {
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;
}
/**
* Cast a variable value to its expansion string.
*
* Non-finite floats are converted explicitly because coercing them to
* string triggers a warning on PHP 8.5.
*
* @param mixed $value
*/
private static function stringifyValue($value): string
{
if (is_float($value) && !is_finite($value)) {
return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
}
return (string) $value;
}
/**
* Stringify non-finite float members of a nested array so that
* http_build_query does not trigger coercion warnings on PHP 8.5.
*
* @param array<array-key,mixed> $value
*
* @return array<array-key,mixed>
*/
private static function stringifyNonFiniteFloats(array $value): array
{
/** @var mixed $member */
foreach ($value as $key => $member) {
if (is_float($member) && !is_finite($member)) {
$value[$key] = self::stringifyValue($member);
} elseif (is_array($member)) {
$value[$key] = self::stringifyNonFiniteFloats($member);
}
}
return $value;
}
private static function encodeValue(string $value, bool $allowReserved): string
{
if ($value === '') {
return '';
}
$matches = [];
if (preg_match_all('/%[0-9A-Fa-f]{2}|./s', $value, $matches) === false) {
throw new RuntimeException('Unable to encode URI template value.');
}
$encoded = '';
foreach ($matches[0] as $token) {
if ($allowReserved && preg_match('/A%[0-9A-Fa-f]{2}z/', $token) === 1) {
$encoded .= $token;
continue;
}
if (preg_match('/A[A-Za-z0-9._~-]z/', $token) === 1) {
$encoded .= $token;
continue;
}
if ($allowReserved && strlen($token) === 1 && strpos(":/?#[]@!$&'()*+,;=", $token) !== false) {
$encoded .= $token;
continue;
}
$encoded .= rawurlencode($token);
}
return $encoded;
}
}