@@ -220,7 +220,11 @@ public function createRequest($method, $urlPath, array $parameters = [], array $
220220 $ urlPath = substr ($ urlPath , strlen ($ this ->url ));
221221 }
222222
223- $ urlPath = $ this ->expandColonParameters ($ urlPath , $ parameters , $ this ->defaultParameters );
223+ if (strpos ($ urlPath , '{ ' ) === FALSE ) {
224+ $ urlPath = $ this ->expandColonParameters ($ urlPath , $ parameters , $ this ->defaultParameters );
225+ } else {
226+ $ urlPath = $ this ->expandUriTemplate ($ urlPath , $ parameters , $ this ->defaultParameters );
227+ }
224228
225229 $ url = rtrim ($ this ->url , '/ ' ) . '/ ' . ltrim ($ urlPath , '/ ' );
226230
@@ -365,4 +369,181 @@ protected function expandColonParameters($url, array $parameters, array $default
365369 return $ url ;
366370 }
367371
372+
373+ /**
374+ * Expands URI template (RFC 6570).
375+ *
376+ * @see http://tools.ietf.org/html/rfc6570
377+ * @todo Inject remaining default parameters into query string?
378+ *
379+ * @param string
380+ * @return string
381+ */
382+ protected function expandUriTemplate ($ url , array $ parameters , array $ defaultParameters )
383+ {
384+ $ parameters += $ defaultParameters ;
385+
386+ static $ operatorFlags = [
387+ '' => ['prefix ' => '' , 'separator ' => ', ' , 'named ' => FALSE , 'ifEmpty ' => '' , 'reserved ' => FALSE ],
388+ '+ ' => ['prefix ' => '' , 'separator ' => ', ' , 'named ' => FALSE , 'ifEmpty ' => '' , 'reserved ' => TRUE ],
389+ '# ' => ['prefix ' => '# ' , 'separator ' => ', ' , 'named ' => FALSE , 'ifEmpty ' => '' , 'reserved ' => TRUE ],
390+ '. ' => ['prefix ' => '. ' , 'separator ' => '. ' , 'named ' => FALSE , 'ifEmpty ' => '' , 'reserved ' => FALSE ],
391+ '/ ' => ['prefix ' => '/ ' , 'separator ' => '/ ' , 'named ' => FALSE , 'ifEmpty ' => '' , 'reserved ' => FALSE ],
392+ '; ' => ['prefix ' => '; ' , 'separator ' => '; ' , 'named ' => TRUE , 'ifEmpty ' => '' , 'reserved ' => FALSE ],
393+ '? ' => ['prefix ' => '? ' , 'separator ' => '& ' , 'named ' => TRUE , 'ifEmpty ' => '= ' , 'reserved ' => FALSE ],
394+ '& ' => ['prefix ' => '& ' , 'separator ' => '& ' , 'named ' => TRUE , 'ifEmpty ' => '= ' , 'reserved ' => FALSE ],
395+ ];
396+
397+ return preg_replace_callback ('~{([+#./;?&])?([^}]+?)}~ ' , function ($ m ) use ($ url , & $ parameters , $ operatorFlags ) {
398+ $ flags = $ operatorFlags [$ m [1 ]];
399+
400+ $ translated = [];
401+ foreach (explode (', ' , $ m [2 ]) as $ name ) {
402+ $ explode = FALSE ;
403+ $ maxLength = NULL ;
404+ if (preg_match ('~^(.+)(?:(\*)|:(\d+))$~ ' , $ name , $ tmp )) { // TODO: Speed up?
405+ $ name = $ tmp [1 ];
406+ if (isset ($ tmp [3 ])) {
407+ $ maxLength = (int ) $ tmp [3 ];
408+ } else {
409+ $ explode = TRUE ;
410+ }
411+ }
412+
413+ if (!isset ($ parameters [$ name ])) { // TODO: Throw exception?
414+ continue ;
415+ }
416+
417+ $ value = $ parameters [$ name ];
418+ if (is_scalar ($ value )) {
419+ $ translated [] = $ this ->prefix ($ flags , $ name , $ this ->escape ($ flags , $ value , $ maxLength ));
420+
421+ } else {
422+ $ value = (array ) $ value ;
423+ $ isAssoc = key ($ value ) !== 0 ;
424+
425+ // The '*' (explode) modifier
426+ if ($ explode ) {
427+ $ parts = [];
428+ if ($ isAssoc ) {
429+ $ this ->walk ($ value , function ($ v , $ k ) use (& $ parts , $ flags , $ maxLength ) {
430+ $ parts [] = $ this ->prefix (['named ' => TRUE ] + $ flags , $ k , $ this ->escape ($ flags , $ v , $ maxLength ));
431+ });
432+
433+ } elseif ($ flags ['named ' ]) {
434+ $ this ->walk ($ value , function ($ v ) use (& $ parts , $ flags , $ name , $ maxLength ) {
435+ $ parts [] = $ this ->prefix ($ flags , $ name , $ this ->escape ($ flags , $ v , $ maxLength ));
436+ });
437+
438+ } else {
439+ $ this ->walk ($ value , function ($ v ) use (& $ parts , $ flags , $ maxLength ) {
440+ $ parts [] = $ this ->escape ($ flags , $ v , $ maxLength );
441+ });
442+ }
443+
444+ if (isset ($ parts [0 ])) {
445+ if ($ flags ['named ' ]) {
446+ $ translated [] = implode ($ flags ['separator ' ], $ parts );
447+ } else {
448+ $ translated [] = $ this ->prefix ($ flags , $ name , implode ($ flags ['separator ' ], $ parts ));
449+ }
450+ }
451+
452+ } else {
453+ $ parts = [];
454+ $ this ->walk ($ value , function ($ v , $ k ) use (& $ parts , $ isAssoc , $ flags , $ maxLength ) {
455+ if ($ isAssoc ) {
456+ $ parts [] = $ this ->escape ($ flags , $ k );
457+ }
458+
459+ $ parts [] = $ this ->escape ($ flags , $ v , $ maxLength );
460+ });
461+
462+ if (isset ($ parts [0 ])) {
463+ $ translated [] = $ this ->prefix ($ flags , $ name , implode (', ' , $ parts ));
464+ }
465+ }
466+ }
467+ }
468+
469+ if (isset ($ translated [0 ])) {
470+ return $ flags ['prefix ' ] . implode ($ flags ['separator ' ], $ translated );
471+ }
472+
473+ return '' ;
474+ }, $ url );
475+ }
476+
477+
478+ /**
479+ * @param array
480+ * @param string
481+ * @param string already escaped
482+ * @return string
483+ */
484+ private function prefix (array $ flags , $ name , $ value )
485+ {
486+ $ prefix = '' ;
487+ if ($ flags ['named ' ]) {
488+ $ prefix .= $ this ->escape ($ flags , $ name );
489+ if (isset ($ value [0 ])) {
490+ $ prefix .= '= ' ;
491+ } else {
492+ $ prefix .= $ flags ['ifEmpty ' ];
493+ }
494+ }
495+
496+ return $ prefix . $ value ;
497+ }
498+
499+
500+ /**
501+ * @param array
502+ * @param mixed
503+ * @param int|NULL
504+ * @return string
505+ */
506+ private function escape (array $ flags , $ value , $ maxLength = NULL )
507+ {
508+ $ value = (string ) $ value ;
509+
510+ if ($ maxLength !== NULL ) {
511+ if (preg_match ('~^(.{ ' . $ maxLength . '}).~u ' , $ value , $ m )) {
512+ $ value = $ m [1 ];
513+ } elseif (strlen ($ value ) > $ maxLength ) { # when malformed UTF-8
514+ $ value = substr ($ value , 0 , $ maxLength );
515+ }
516+ }
517+
518+ if ($ flags ['reserved ' ]) {
519+ $ parts = preg_split ('~(%[0-9a-fA-F]{2}|[:/?#[\]@!$& \'()*+,;=])~ ' , $ value , -1 , PREG_SPLIT_DELIM_CAPTURE );
520+ $ parts [] = '' ;
521+
522+ $ escaped = '' ;
523+ for ($ i = 0 , $ count = count ($ parts ); $ i < $ count ; $ i += 2 ) {
524+ $ escaped .= rawurlencode ($ parts [$ i ]) . $ parts [$ i + 1 ];
525+ }
526+
527+ return $ escaped ;
528+ }
529+
530+ return rawurlencode ($ value );
531+ }
532+
533+
534+ /**
535+ * @param array
536+ * @param callable
537+ */
538+ private function walk (array $ array , $ cb )
539+ {
540+ foreach ($ array as $ k => $ v ) {
541+ if ($ v === NULL ) {
542+ continue ;
543+ }
544+
545+ $ cb ($ v , $ k );
546+ }
547+ }
548+
368549}
0 commit comments