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]
 
   30     private static $delims = [
':', 
'/', 
'?', 
'#', 
'[', 
']', 
'@', 
'!', 
'$',
 
   31         '&', 
'\'', 
'(', 
')', 
'*', 
'+', 
',', 
';', 
'='];
 
   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'],
 
   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;
 
   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) {
 
  110             if (!isset($this->variables[$value[
'value']])) {
 
  114             $variable = $this->variables[$value[
'value']];
 
  115             $actuallyUseQuery = $useQuery;
 
  118             if (is_array($variable)) {
 
  119                 $isAssoc = $this->isAssoc($variable);
 
  121                 foreach ($variable as $key => $var) {
 
  123                         $key = rawurlencode($key);
 
  124                         $isNestedArray = is_array($var);
 
  126                         $isNestedArray = 
false;
 
  129                     if (!$isNestedArray) {
 
  130                         $var = rawurlencode($var);
 
  131                         if ($parsed[
'operator'] === 
'+' ||
 
  132                             $parsed[
'operator'] === 
'#' 
  134                             $var = $this->decodeReserved($var);
 
  138                     if ($value[
'modifier'] === 
'*') {
 
  140                             if ($isNestedArray) {
 
  144                                     http_build_query([$key => $var]),
 
  148                                 $var = $key . 
'=' . $var;
 
  150                         } elseif ($key > 0 && $actuallyUseQuery) {
 
  151                             $var = $value[
'value'] . 
'=' . $var;
 
  158                 if (empty($variable)) {
 
  159                     $actuallyUseQuery = 
false;
 
  160                 } elseif ($value[
'modifier'] === 
'*') {
 
  161                     $expanded = implode($joiner, $kvp);
 
  165                         $actuallyUseQuery = 
false;
 
  173                         foreach ($kvp as $k => &$v) {
 
  177                     $expanded = implode(
',', $kvp);
 
  180                 if ($value[
'modifier'] === 
':') {
 
  181                     $variable = substr($variable, 0, $value[
'position']);
 
  183                 $expanded = rawurlencode($variable);
 
  184                 if ($parsed[
'operator'] === 
'+' || $parsed[
'operator'] === 
'#') {
 
  185                     $expanded = $this->decodeReserved($expanded);
 
  189             if ($actuallyUseQuery) {
 
  190                 if (!$expanded && $joiner !== 
'&') {
 
  191                     $expanded = $value[
'value'];
 
  193                     $expanded = $value[
'value'] . 
'=' . $expanded;
 
  197             $replacements[] = $expanded;
 
  200         $ret = implode($joiner, $replacements);
 
  201         if ($ret && $prefix) {
 
  202             return $prefix . $ret;
 
  220     private function isAssoc(array $array)
 
  222         return $array && array_keys($array)[0] !== 0;
 
  233     private function decodeReserved($string)
 
  235         return str_replace(self::$delimsPct, self::$delims, $string);