CHANGELOG.md000064400000001406144760112200006353 0ustar00# Changelog All notable changes to `uri-template` will be documented in this file. Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. ## v1.0.0 - 2021-08-14 ### Changed - Dropped support for PHP 7.1 ## v0.2.0 - 2020-07-21 ### Added - Support PHP 7.1 and 8.0 ### Changed - Renamed `GuzzleHttp\Utility\` to `GuzzleHttp\UriTemplate\` ### Fixed - Delegate RFC 3986 query string encoding to PHP - Fixed some bugs when parts ofs values are not strings ## v0.1.1 - 2020-06-30 ### Fixed - Fixed an error due to strict_types [d47d1b0a8e78a3fac1cd0f69d675fc9e06771ac8](https://github.com/guzzle/uri-template/commit/d47d1b0a8e78a3fac1cd0f69d675fc9e06771ac8) ## v0.1.0 - 2020-06-30 ### Added - Moved the `UriTemplate` class in this package LICENSE.md000064400000002156144760112200006151 0ustar00# The MIT License (MIT) Copyright (c) 2020 George Mponos > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in > all copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN > THE SOFTWARE. README.md000064400000000723144760112200006022 0ustar00# uri-template ## Install Via Composer ``` bash $ composer require guzzlehttp/uri-template ``` ## Change log Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Testing ``` bash $ make test ``` ## Security If you discover any security related issues, please email security@guzzlephp.org instead of using the issue tracker. ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. composer.json000064400000002755144760112200007274 0ustar00{ "name": "guzzlehttp/uri-template", "type": "library", "description": "A polyfill class for uri_template of PHP", "keywords": [ "guzzlehttp", "uri-template" ], "homepage": "https://github.com/guzzlehttp/uri-template", "license": "MIT", "authors": [ { "name": "George Mponos", "email": "gmponos@gmail.com", "homepage": "https://github.com/gmponos", "role": "Developer" } ], "repositories": [ { "type": "package", "package": { "name": "uri-template/tests", "version": "1.0.0", "dist": { "url": "https://github.com/uri-templates/uritemplate-test/archive/520fdd8b0f78779d12178c357a986e0e727f4bd0.zip", "type": "zip" } } } ], "require": { "php" : "^7.2.5 || ^8.0", "symfony/polyfill-php80": "^1.17" }, "require-dev": { "phpunit/phpunit" : "^8.5.19 || ^9.5.8", "uri-template/tests": "1.0.0" }, "autoload": { "psr-4": { "GuzzleHttp\\UriTemplate\\": "src" } }, "autoload-dev": { "psr-4": { "GuzzleHttp\\UriTemplate\\Tests\\": "tests" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } }, "config": { "preferred-install": "dist", "sort-packages": true } } src/UriTemplate.php000064400000022566144760112200010307 0ustar00 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 $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 $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 $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.5 if (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} */ 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); } }