├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src └── UriTemplate.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `uri-template` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## v1.0.4 - 2025-02-03 8 | 9 | ### Changed 10 | - Officially support PHP 8.4 11 | 12 | ## v1.0.3 - 2023-12-03 13 | 14 | ### Changed 15 | - Updated link to RFC 6570 16 | 17 | ## v1.0.2 - 2023-08-27 18 | 19 | ### Changed 20 | - Officially support PHP 8.2 and 8.3 21 | 22 | ### Fixed 23 | - Fixed using `0` as an expanded value 24 | 25 | ## v1.0.1 - 2021-10-07 26 | 27 | ### Changed 28 | - Officially support PHP 8.1 29 | 30 | ## v1.0.0 - 2021-08-14 31 | 32 | ### Changed 33 | - Dropped support for PHP 7.1 34 | 35 | ## v0.2.0 - 2020-07-21 36 | 37 | ### Added 38 | - Support PHP 7.1 and 8.0 39 | 40 | ### Changed 41 | - Renamed `GuzzleHttp\Utility\` to `GuzzleHttp\UriTemplate\` 42 | 43 | ### Fixed 44 | - Delegate RFC 3986 query string encoding to PHP 45 | - Fixed some bugs when parts ofs values are not strings 46 | 47 | ## v0.1.1 - 2020-06-30 48 | 49 | ### Fixed 50 | - Fixed an error due to strict_types [d47d1b0a8e78a3fac1cd0f69d675fc9e06771ac8](https://github.com/guzzle/uri-template/commit/d47d1b0a8e78a3fac1cd0f69d675fc9e06771ac8) 51 | 52 | ## v0.1.0 - 2020-06-30 53 | 54 | ### Added 55 | - Moved the `UriTemplate` class in this package 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Dowling 4 | Copyright (c) 2020 George Mponos 5 | Copyright (c) 2020 Graham Campbell 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uri-template 2 | 3 | ## Install 4 | 5 | Via Composer 6 | 7 | ``` bash 8 | $ composer require guzzlehttp/uri-template 9 | ``` 10 | 11 | ## Change log 12 | 13 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 14 | 15 | ## Testing 16 | 17 | ``` bash 18 | $ make test 19 | ``` 20 | 21 | ## Security 22 | 23 | If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/uri-template/security/policy) for more information. 24 | 25 | ## License 26 | 27 | Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. 28 | 29 | ## For Enterprise 30 | 31 | Available as part of the Tidelift Subscription 32 | 33 | The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-uri-template?utm_source=packagist-guzzlehttp-uri-template7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guzzlehttp/uri-template", 3 | "description": "A polyfill class for uri_template of PHP", 4 | "keywords": [ 5 | "guzzlehttp", 6 | "uri-template" 7 | ], 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Graham Campbell", 12 | "email": "hello@gjcampbell.co.uk", 13 | "homepage": "https://github.com/GrahamCampbell" 14 | }, 15 | { 16 | "name": "Michael Dowling", 17 | "email": "mtdowling@gmail.com", 18 | "homepage": "https://github.com/mtdowling" 19 | }, 20 | { 21 | "name": "George Mponos", 22 | "email": "gmponos@gmail.com", 23 | "homepage": "https://github.com/gmponos" 24 | }, 25 | { 26 | "name": "Tobias Nyholm", 27 | "email": "tobias.nyholm@gmail.com", 28 | "homepage": "https://github.com/Nyholm" 29 | } 30 | ], 31 | "repositories": [ 32 | { 33 | "type": "package", 34 | "package": { 35 | "name": "uri-template/tests", 36 | "version": "1.0.0", 37 | "dist": { 38 | "url": "https://github.com/uri-templates/uritemplate-test/archive/520fdd8b0f78779d12178c357a986e0e727f4bd0.zip", 39 | "type": "zip" 40 | } 41 | } 42 | } 43 | ], 44 | "require": { 45 | "php" : "^7.2.5 || ^8.0", 46 | "symfony/polyfill-php80": "^1.24" 47 | }, 48 | "require-dev": { 49 | "bamarni/composer-bin-plugin": "^1.8.2", 50 | "phpunit/phpunit" : "^8.5.36 || ^9.6.15", 51 | "uri-template/tests": "1.0.0" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "GuzzleHttp\\UriTemplate\\": "src" 56 | } 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "GuzzleHttp\\UriTemplate\\Tests\\": "tests" 61 | } 62 | }, 63 | "extra": { 64 | "bamarni-bin": { 65 | "bin-links": true, 66 | "forward-command": false 67 | } 68 | }, 69 | "config": { 70 | "allow-plugins": { 71 | "bamarni/composer-bin-plugin": true 72 | }, 73 | "preferred-install": "dist", 74 | "sort-packages": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/UriTemplate.php: -------------------------------------------------------------------------------- 1 | Hash for quick operator lookups 16 | */ 17 | private static $operatorHash = [ 18 | '' => ['prefix' => '', 'joiner' => ',', 'query' => false], 19 | '+' => ['prefix' => '', 'joiner' => ',', 'query' => false], 20 | '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false], 21 | '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false], 22 | '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false], 23 | ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true], 24 | '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true], 25 | '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true], 26 | ]; 27 | 28 | /** 29 | * @var string[] Delimiters 30 | */ 31 | private static $delims = [ 32 | ':', 33 | '/', 34 | '?', 35 | '#', 36 | '[', 37 | ']', 38 | '@', 39 | '!', 40 | '$', 41 | '&', 42 | '\'', 43 | '(', 44 | ')', 45 | '*', 46 | '+', 47 | ',', 48 | ';', 49 | '=', 50 | ]; 51 | 52 | /** 53 | * @var string[] Percent encoded delimiters 54 | */ 55 | private static $delimsPct = [ 56 | '%3A', 57 | '%2F', 58 | '%3F', 59 | '%23', 60 | '%5B', 61 | '%5D', 62 | '%40', 63 | '%21', 64 | '%24', 65 | '%26', 66 | '%27', 67 | '%28', 68 | '%29', 69 | '%2A', 70 | '%2B', 71 | '%2C', 72 | '%3B', 73 | '%3D', 74 | ]; 75 | 76 | /** 77 | * @param array $variables Variables to use in the template expansion 78 | * 79 | * @throws \RuntimeException 80 | */ 81 | public static function expand(string $template, array $variables): string 82 | { 83 | if (false === \strpos($template, '{')) { 84 | return $template; 85 | } 86 | 87 | /** @var string|null */ 88 | $result = \preg_replace_callback( 89 | '/\{([^\}]+)\}/', 90 | self::expandMatchCallback($variables), 91 | $template 92 | ); 93 | 94 | if (null === $result) { 95 | throw new \RuntimeException(\sprintf('Unable to process template: %s', \preg_last_error_msg())); 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | /** 102 | * @param array $variables Variables to use in the template expansion 103 | * 104 | * @return callable(string[]): string 105 | */ 106 | private static function expandMatchCallback(array $variables): callable 107 | { 108 | return static function (array $matches) use ($variables): string { 109 | return self::expandMatch($matches, $variables); 110 | }; 111 | } 112 | 113 | /** 114 | * Process an expansion 115 | * 116 | * @param array $variables Variables to use in the template expansion 117 | * @param string[] $matches Matches met in the preg_replace_callback 118 | * 119 | * @return string Returns the replacement string 120 | */ 121 | private static function expandMatch(array $matches, array $variables): string 122 | { 123 | $replacements = []; 124 | $parsed = self::parseExpression($matches[1]); 125 | $prefix = self::$operatorHash[$parsed['operator']]['prefix']; 126 | $joiner = self::$operatorHash[$parsed['operator']]['joiner']; 127 | $useQuery = self::$operatorHash[$parsed['operator']]['query']; 128 | $allUndefined = true; 129 | 130 | foreach ($parsed['values'] as $value) { 131 | if (!isset($variables[$value['value']])) { 132 | continue; 133 | } 134 | 135 | $variable = $variables[$value['value']]; 136 | $actuallyUseQuery = $useQuery; 137 | $expanded = ''; 138 | 139 | if (\is_array($variable)) { 140 | $isAssoc = self::isAssoc($variable); 141 | $kvp = []; 142 | /** @var mixed $var */ 143 | foreach ($variable as $key => $var) { 144 | if ($isAssoc) { 145 | $key = \rawurlencode((string) $key); 146 | $isNestedArray = \is_array($var); 147 | } else { 148 | $isNestedArray = false; 149 | } 150 | 151 | if (!$isNestedArray) { 152 | $var = \rawurlencode((string) $var); 153 | if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { 154 | $var = self::decodeReserved($var); 155 | } 156 | } 157 | 158 | if ($value['modifier'] === '*') { 159 | if ($isAssoc) { 160 | if ($isNestedArray) { 161 | // Nested arrays must allow for deeply nested structures. 162 | $var = \http_build_query([$key => $var], '', '&', \PHP_QUERY_RFC3986); 163 | } else { 164 | $var = \sprintf('%s=%s', (string) $key, (string) $var); 165 | } 166 | } elseif ($key > 0 && $actuallyUseQuery) { 167 | $var = \sprintf('%s=%s', $value['value'], (string) $var); 168 | } 169 | } 170 | 171 | /** @var string $var */ 172 | $kvp[$key] = $var; 173 | } 174 | 175 | if (0 === \count($variable)) { 176 | $actuallyUseQuery = false; 177 | } elseif ($value['modifier'] === '*') { 178 | $expanded = \implode($joiner, $kvp); 179 | if ($isAssoc) { 180 | // Don't prepend the value name when using the explode 181 | // modifier with an associative array. 182 | $actuallyUseQuery = false; 183 | } 184 | } else { 185 | if ($isAssoc) { 186 | // When an associative array is encountered and the 187 | // explode modifier is not set, then the result must be 188 | // a comma separated list of keys followed by their 189 | // respective values. 190 | foreach ($kvp as $k => &$v) { 191 | $v = \sprintf('%s,%s', $k, $v); 192 | } 193 | } 194 | $expanded = \implode(',', $kvp); 195 | } 196 | } else { 197 | $allUndefined = false; 198 | if ($value['modifier'] === ':' && isset($value['position'])) { 199 | $variable = \substr((string) $variable, 0, $value['position']); 200 | } 201 | $expanded = \rawurlencode((string) $variable); 202 | if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { 203 | $expanded = self::decodeReserved($expanded); 204 | } 205 | } 206 | 207 | if ($actuallyUseQuery) { 208 | if ($expanded === '' && $joiner !== '&') { 209 | $expanded = $value['value']; 210 | } else { 211 | $expanded = \sprintf('%s=%s', $value['value'], $expanded); 212 | } 213 | } 214 | 215 | $replacements[] = $expanded; 216 | } 217 | 218 | $ret = \implode($joiner, $replacements); 219 | 220 | if ('' === $ret) { 221 | // Spec section 3.2.4 and 3.2.5 222 | if (false === $allUndefined && ('#' === $prefix || '.' === $prefix)) { 223 | return $prefix; 224 | } 225 | } else { 226 | if ('' !== $prefix) { 227 | return \sprintf('%s%s', $prefix, $ret); 228 | } 229 | } 230 | 231 | return $ret; 232 | } 233 | 234 | /** 235 | * Parse an expression into parts 236 | * 237 | * @param string $expression Expression to parse 238 | * 239 | * @return array{operator:string, values:array} 240 | */ 241 | private static function parseExpression(string $expression): array 242 | { 243 | $result = []; 244 | 245 | if (isset(self::$operatorHash[$expression[0]])) { 246 | $result['operator'] = $expression[0]; 247 | /** @var string */ 248 | $expression = \substr($expression, 1); 249 | } else { 250 | $result['operator'] = ''; 251 | } 252 | 253 | $result['values'] = []; 254 | foreach (\explode(',', $expression) as $value) { 255 | $value = \trim($value); 256 | $varspec = []; 257 | if ($colonPos = \strpos($value, ':')) { 258 | $varspec['value'] = (string) \substr($value, 0, $colonPos); 259 | $varspec['modifier'] = ':'; 260 | $varspec['position'] = (int) \substr($value, $colonPos + 1); 261 | } elseif (\substr($value, -1) === '*') { 262 | $varspec['modifier'] = '*'; 263 | $varspec['value'] = (string) \substr($value, 0, -1); 264 | } else { 265 | $varspec['value'] = $value; 266 | $varspec['modifier'] = ''; 267 | } 268 | $result['values'][] = $varspec; 269 | } 270 | 271 | return $result; 272 | } 273 | 274 | /** 275 | * Determines if an array is associative. 276 | * 277 | * This makes the assumption that input arrays are sequences or hashes. 278 | * This assumption is a tradeoff for accuracy in favor of speed, but it 279 | * should work in almost every case where input is supplied for a URI 280 | * template. 281 | */ 282 | private static function isAssoc(array $array): bool 283 | { 284 | return $array && \array_keys($array)[0] !== 0; 285 | } 286 | 287 | /** 288 | * Removes percent encoding on reserved characters (used with + and # 289 | * modifiers). 290 | */ 291 | private static function decodeReserved(string $string): string 292 | { 293 | return \str_replace(self::$delimsPct, self::$delims, $string); 294 | } 295 | } 296 | --------------------------------------------------------------------------------