Skip to content

Commit e110135

Browse files
committed
Api: implemented support for URI Templates (RFC 6570)
1 parent 3616d61 commit e110135

2 files changed

Lines changed: 653 additions & 1 deletion

File tree

src/Github/Api.php

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)