5 * Expands URI templates. Userland implementation of PECL uri_template.
7 * @link http://tools.ietf.org/html/rfc6570
11 /** @var string URI template */
14 /** @var array Variables to use in the template expansion */
17 /** @var array Hash for quick operator lookups */
18 private static $operatorHash = [
19 '' => ['prefix' => '', 'joiner' => ',', 'query' => false],
20 '+' => ['prefix' => '', 'joiner' => ',', 'query' => false],
21 '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false],
22 '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false],
23 '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false],
24 ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true],
25 '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true],
26 '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true]
29 /** @var array Delimiters */
30 private static $delims = [':', '/', '?', '#', '[', ']', '@', '!', '$',
31 '&', '\'', '(', ')', '*', '+', ',', ';', '='];
33 /** @var array Percent encoded delimiters */
34 private static $delimsPct = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D',
35 '%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C',
38 public function expand($template, array $variables)
40 if (false === strpos($template, '{')) {
44 $this->template = $template;
45 $this->variables = $variables;
47 return preg_replace_callback(
49 [$this, 'expandMatch'],
55 * Parse an expression into parts
57 * @param string $expression Expression to parse
59 * @return array Returns an associative array of parts
61 private function parseExpression($expression)
65 if (isset(self::$operatorHash[$expression[0]])) {
66 $result['operator'] = $expression[0];
67 $expression = substr($expression, 1);
69 $result['operator'] = '';
72 foreach (explode(',', $expression) as $value) {
73 $value = trim($value);
75 if ($colonPos = strpos($value, ':')) {
76 $varspec['value'] = substr($value, 0, $colonPos);
77 $varspec['modifier'] = ':';
78 $varspec['position'] = (int) substr($value, $colonPos + 1);
79 } elseif (substr($value, -1) === '*') {
80 $varspec['modifier'] = '*';
81 $varspec['value'] = substr($value, 0, -1);
83 $varspec['value'] = (string) $value;
84 $varspec['modifier'] = '';
86 $result['values'][] = $varspec;
93 * Process an expansion
95 * @param array $matches Matches met in the preg_replace_callback
97 * @return string Returns the replacement string
99 private function expandMatch(array $matches)
101 static $rfc1738to3986 = ['+' => '%20', '%7e' => '~'];
104 $parsed = self::parseExpression($matches[1]);
105 $prefix = self::$operatorHash[$parsed['operator']]['prefix'];
106 $joiner = self::$operatorHash[$parsed['operator']]['joiner'];
107 $useQuery = self::$operatorHash[$parsed['operator']]['query'];
109 foreach ($parsed['values'] as $value) {
111 if (!isset($this->variables[$value['value']])) {
115 $variable = $this->variables[$value['value']];
116 $actuallyUseQuery = $useQuery;
119 if (is_array($variable)) {
121 $isAssoc = $this->isAssoc($variable);
123 foreach ($variable as $key => $var) {
126 $key = rawurlencode($key);
127 $isNestedArray = is_array($var);
129 $isNestedArray = false;
132 if (!$isNestedArray) {
133 $var = rawurlencode($var);
134 if ($parsed['operator'] === '+' ||
135 $parsed['operator'] === '#'
137 $var = $this->decodeReserved($var);
141 if ($value['modifier'] === '*') {
143 if ($isNestedArray) {
144 // Nested arrays must allow for deeply nested
147 http_build_query([$key => $var]),
151 $var = $key . '=' . $var;
153 } elseif ($key > 0 && $actuallyUseQuery) {
154 $var = $value['value'] . '=' . $var;
161 if (empty($variable)) {
162 $actuallyUseQuery = false;
163 } elseif ($value['modifier'] === '*') {
164 $expanded = implode($joiner, $kvp);
166 // Don't prepend the value name when using the explode
167 // modifier with an associative array.
168 $actuallyUseQuery = false;
172 // When an associative array is encountered and the
173 // explode modifier is not set, then the result must be
174 // a comma separated list of keys followed by their
175 // respective values.
176 foreach ($kvp as $k => &$v) {
180 $expanded = implode(',', $kvp);
184 if ($value['modifier'] === ':') {
185 $variable = substr($variable, 0, $value['position']);
187 $expanded = rawurlencode($variable);
188 if ($parsed['operator'] === '+' || $parsed['operator'] === '#') {
189 $expanded = $this->decodeReserved($expanded);
193 if ($actuallyUseQuery) {
194 if (!$expanded && $joiner !== '&') {
195 $expanded = $value['value'];
197 $expanded = $value['value'] . '=' . $expanded;
201 $replacements[] = $expanded;
204 $ret = implode($joiner, $replacements);
205 if ($ret && $prefix) {
206 return $prefix . $ret;
213 * Determines if an array is associative.
215 * This makes the assumption that input arrays are sequences or hashes.
216 * This assumption is a tradeoff for accuracy in favor of speed, but it
217 * should work in almost every case where input is supplied for a URI
220 * @param array $array Array to check
224 private function isAssoc(array $array)
226 return $array && array_keys($array)[0] !== 0;
230 * Removes percent encoding on reserved characters (used with + and #
233 * @param string $string String to fix
237 private function decodeReserved($string)
239 return str_replace(self::$delimsPct, self::$delims, $string);