allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,98 @@
<?php
namespace Rize;
use Rize\UriTemplate\Parser;
/**
* URI Template.
*/
class UriTemplate
{
protected Parser $parser;
protected array $parsed = [];
public function __construct(protected string $base_uri = '', protected array $params = [], ?Parser $parser = null)
{
$this->parser = $parser ?: $this->createNodeParser();
}
/**
* Expands URI Template.
*
* @param mixed $params
*/
public function expand(string $uri, $params = []): string
{
$params += $this->params;
$uri = $this->base_uri . $uri;
$result = [];
// quick check
if (!str_contains($uri, '{')) {
return $uri;
}
$parser = $this->parser;
$nodes = $parser->parse($uri);
foreach ($nodes as $node) {
$result[] = $node->expand($parser, $params);
}
return implode('', $result);
}
/**
* Extracts variables from URI.
*
* @return null|array params or null if not match and $strict is true
*/
public function extract(string $template, string $uri, bool $strict = false): ?array
{
$params = [];
$nodes = $this->parser->parse($template);
// PHP 8.1.0RC4-dev still throws deprecation warning for `strlen`.
// $uri = (string) $uri;
foreach ($nodes as $node) {
// if strict is given, and there's no remaining uri just return null
if ($strict && (string) $uri === '') {
return null;
}
// URI will be truncated from the start when a match is found
$match = $node->match($this->parser, $uri, $params, $strict);
if ($match === null) {
return null;
}
[$uri, $params] = $match;
}
// if there's remaining $uri, matching is failed
if ($strict && (string) $uri !== '') {
return null;
}
return $params;
}
public function getParser(): Parser
{
return $this->parser;
}
protected function createNodeParser(): Parser
{
static $parser;
if ($parser) {
return $parser;
}
return $parser = new Parser();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Rize\UriTemplate\Node;
use Rize\UriTemplate\Parser;
/**
* Base class for all Nodes.
*/
abstract class Abstraction
{
public function __construct(private readonly string $token) {}
/**
* Expands URI template
*
* @param array<string, mixed> $params
*/
public function expand(Parser $parser, array $params = []): ?string
{
return $this->token;
}
/**
* Matches given URI against current node.
*
* @param array<string, mixed> $params
*
* @return null|array{0: string, 1: array<string, mixed>} `uri and params` or `null` if not match and $strict is true
*/
public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array
{
// match literal string from start to end
if (str_starts_with($uri, $this->token)) {
$uri = substr($uri, strlen($this->token));
}
// when there's no match, just return null if strict mode is given
elseif ($strict) {
return null;
}
return [$uri, $params];
}
public function getToken(): string
{
return $this->token;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Rize\UriTemplate\Node;
use Rize\UriTemplate\Operator;
use Rize\UriTemplate\Parser;
class Expression extends Abstraction
{
/**
* @param string $forwardLookupSeparator
*/
public function __construct(string $token, private readonly Operator\Abstraction $operator, private readonly ?array $variables = null, /**
* Whether to do a forward lookup for a given separator.
*/
private $forwardLookupSeparator = null)
{
parent::__construct($token);
}
public function getOperator(): Operator\Abstraction
{
return $this->operator;
}
public function getVariables(): ?array
{
return $this->variables;
}
public function getForwardLookupSeparator(): string
{
return $this->forwardLookupSeparator;
}
public function setForwardLookupSeparator(string $forwardLookupSeparator): void
{
$this->forwardLookupSeparator = $forwardLookupSeparator;
}
public function expand(Parser $parser, array $params = []): ?string
{
$data = [];
$op = $this->operator;
if ($this->variables === null) {
return $op->first;
}
// check for variable modifiers
foreach ($this->variables as $var) {
$val = $op->expand($parser, $var, $params);
// skip null value
if (!is_null($val)) {
$data[] = $val;
}
}
return $data ? $op->first . implode($op->sep, $data) : null;
}
/**
* Matches given URI against current node.
*
* @return null|array `uri and params` or `null` if not match and $strict is true
*/
public function match(Parser $parser, string $uri, array $params = [], bool $strict = false): ?array
{
$op = $this->operator;
// check expression operator first
if ($op->id && isset($uri[0]) && $uri[0] !== $op->id) {
return [$uri, $params];
}
// remove operator from input
if ($op->id) {
$uri = substr($uri, 1);
}
foreach ($this->sortVariables($this->variables) as $var) {
$regex = '#' . $op->toRegex($parser, $var) . '#';
$val = null;
// do a forward lookup and get just the relevant part
$remainingUri = '';
$preparedUri = $uri;
if ($this->forwardLookupSeparator) {
$lastOccurrenceOfSeparator = stripos($uri, $this->forwardLookupSeparator);
$preparedUri = substr($uri, 0, $lastOccurrenceOfSeparator);
$remainingUri = substr($uri, $lastOccurrenceOfSeparator);
}
if (preg_match($regex, $preparedUri, $match)) {
// remove matched part from input
$preparedUri = preg_replace($regex, '', $preparedUri, 1);
$val = $op->extract($parser, $var, $match[0]);
}
// if strict is given, we quit immediately when there's no match
elseif ($strict) {
return null;
}
$uri = $preparedUri . $remainingUri;
$params[$var->getToken()] = $val;
}
return [$uri, $params];
}
/**
* Sort variables before extracting data from uri.
* We have to sort vars by non-explode to explode.
*/
protected function sortVariables(array $vars): array
{
usort($vars, static fn($a, $b) => $a->options['modifier'] <=> $b->options['modifier']);
return $vars;
}
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Rize\UriTemplate\Node;
class Literal extends Abstraction {}

View File

@@ -0,0 +1,27 @@
<?php
namespace Rize\UriTemplate\Node;
class Variable extends Abstraction
{
/**
* Variable name without modifier
* e.g. 'term:1' becomes 'term'.
*/
public string $name;
public array $options = ['modifier' => null, 'value' => null];
public function __construct(string $token, array $options = [])
{
parent::__construct($token);
$this->options = $options + $this->options;
// normalize var name e.g. from 'term:1' becomes 'term'
$name = $token;
if ($options['modifier'] === ':') {
$name = strstr($name, $options['modifier'], true);
}
$this->name = $name;
}
}

View File

@@ -0,0 +1,373 @@
<?php
namespace Rize\UriTemplate\Operator;
use Rize\UriTemplate\Node;
use Rize\UriTemplate\Parser;
/**
* .------------------------------------------------------------------.
* | NUL + . / ; ? & # |
* |------------------------------------------------------------------|
* | first | "" "" "." "/" ";" "?" "&" "#" |
* | sep | "," "," "." "/" ";" "&" "&" "," |
* | named | false false false false true true true false |
* | ifemp | "" "" "" "" "" "=" "=" "" |
* | allow | U U+R U U U U U U+R |
* `------------------------------------------------------------------'.
*
* named = false
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
* | 2 | {/list*} /red/green/blue | {$value}+(?:{$sep}{$value}+)*
* | 3 | {/keys} /semi,%3B,dot,.,comma,%2C | /(\w+,?)+
* | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*
* named = true
* | 1 | {?list} ?list=red,green,blue | {name}=(?:\w+(?:,\w+?)*)*
* | 2 | {?list*} ?list=red&list=green&list=blue | {name}+=(?:{$value}+(?:{sep}{name}+={$value}*))*
* | 3 | {?keys} ?keys=semi,%3B,dot,.,comma,%2C | (same as 1)
* | 4 | {?keys*} ?semi=%3B&dot=.&comma=%2C | (same as 2)
*
* UNRESERVED
* ----------
* RFC 1738 ALPHA | DIGIT | "-" | "." | "_" | | "$" | "+" | "!" | "*" | "'" | "(" | ")" | ","
* RFC 3986 ALPHA | DIGIT | "-" | "." | "_" | "~"
* RFC 6570 ALPHA | DIGIT | "-" | "." | "_" | "~"
*
* RESERVED
* --------
* RFC 1738 ":" | "/" | "?" | | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" | "-" | "_" | "." |
* RFC 3986 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
* RFC 6570 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
*
* PHP_QUERY_RFC3986 was added in PHP 5.4.0
*/
abstract class Abstraction
{
/**
* start - Variable offset position, level-2 operators start at 1
* (exclude operator itself, e.g. {?query})
* first - If variables found, prepend this value to it
* named - Whether the expansion includes the variable or key name
* reserved - union of (unreserved / reserved / pct-encoded).
*/
public $id;
public $named;
public $sep;
public $empty;
public $reserved;
public $start;
public $first;
/**
* gen-delims | sub-delims.
*/
public static $reserved_chars = [
'%3A' => ':',
'%2F' => '/',
'%3F' => '?',
'%23' => '#',
'%5B' => '[',
'%5D' => ']',
'%40' => '@',
'%21' => '!',
'%24' => '$',
'%26' => '&',
'%27' => "'",
'%28' => '(',
'%29' => ')',
'%2A' => '*',
'%2B' => '+',
'%2C' => ',',
'%3B' => ';',
'%3D' => '=',
];
protected static $types = [
'' => [
'sep' => ',',
'named' => false,
'empty' => '',
'reserved' => false,
'start' => 0,
'first' => null,
],
'+' => [
'sep' => ',',
'named' => false,
'empty' => '',
'reserved' => true,
'start' => 1,
'first' => null,
],
'.' => [
'sep' => '.',
'named' => false,
'empty' => '',
'reserved' => false,
'start' => 1,
'first' => '.',
],
'/' => [
'sep' => '/',
'named' => false,
'empty' => '',
'reserved' => false,
'start' => 1,
'first' => '/',
],
';' => [
'sep' => ';',
'named' => true,
'empty' => '',
'reserved' => false,
'start' => 1,
'first' => ';',
],
'?' => [
'sep' => '&',
'named' => true,
'empty' => '=',
'reserved' => false,
'start' => 1,
'first' => '?',
],
'&' => [
'sep' => '&',
'named' => true,
'empty' => '=',
'reserved' => false,
'start' => 1,
'first' => '&',
],
'#' => [
'sep' => ',',
'named' => false,
'empty' => '',
'reserved' => true,
'start' => 1,
'first' => '#',
],
];
protected static $loaded = [];
/**
* RFC 3986 Allowed path characters regex except the path delimiter '/'.
*
* @var string
*/
protected static $pathRegex = '(?:[a-zA-Z0-9\-\._~!\$&\'\(\)\*\+,;=%:@]+|%(?![A-Fa-f0-9]{2}))';
/**
* RFC 3986 Allowed query characters regex except the query parameter delimiter '&'.
*
* @var string
*/
protected static $queryRegex = '(?:[a-zA-Z0-9\-\._~!\$\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))';
public function __construct($id, $named, $sep, $empty, $reserved, $start, $first)
{
$this->id = $id;
$this->named = $named;
$this->sep = $sep;
$this->empty = $empty;
$this->start = $start;
$this->first = $first;
$this->reserved = $reserved;
}
abstract public function toRegex(Parser $parser, Node\Variable $var): string;
public function expand(Parser $parser, Node\Variable $var, array $params = [])
{
$options = $var->options;
$name = $var->name;
$is_explode = in_array($options['modifier'], ['*', '%']);
// skip null
if (!isset($params[$name])) {
return null;
}
$val = $params[$name];
// This algorithm is based on RFC6570 http://tools.ietf.org/html/rfc6570
// non-array, e.g. string
if (!is_array($val)) {
return $this->expandString($parser, $var, $val);
}
// non-explode ':'
if (!$is_explode) {
return $this->expandNonExplode($parser, $var, $val);
}
// explode '*', '%'
return $this->expandExplode($parser, $var, $val);
}
public function expandString(Parser $parser, Node\Variable $var, $val)
{
$val = (string) $val;
$options = $var->options;
$result = null;
if ($options['modifier'] === ':') {
$val = substr($val, 0, (int) $options['value']);
}
return $result . $this->encode($parser, $var, $val);
}
/**
* Non explode modifier ':'.
*/
public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
return $this->encode($parser, $var, $val);
}
/**
* Explode modifier '*', '%'.
*/
public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
return $this->encode($parser, $var, $val);
}
/**
* Encodes variable according to spec (reserved or unreserved).
*
* @return string encoded string
*/
public function encode(Parser $parser, Node\Variable $var, mixed $values)
{
$values = (array) $values;
$list = isset($values[0]);
$reserved = $this->reserved;
$maps = static::$reserved_chars;
$sep = $this->sep;
$assoc_sep = '=';
// non-explode modifier always use ',' as a separator
if ($var->options['modifier'] !== '*') {
$assoc_sep = $sep = ',';
}
array_walk($values, function (&$v, $k) use ($assoc_sep, $reserved, $list, $maps): void {
$encoded = rawurlencode($v);
// assoc? encode key too
if (!$list) {
$encoded = rawurlencode($k) . $assoc_sep . $encoded;
}
// rawurlencode is compliant with 'unreserved' set
if (!$reserved) {
$v = $encoded;
}
// decode chars in reserved set
else {
$v = str_replace(
array_keys($maps),
$maps,
$encoded,
);
}
});
return implode($sep, $values);
}
/**
* Decodes variable.
*
* @return string decoded string
*/
public function decode(Parser $parser, Node\Variable $var, mixed $values)
{
$single = !is_array($values);
$values = (array) $values;
array_walk($values, function (&$v, $k): void {
$v = rawurldecode($v);
});
return $single ? reset($values) : $values;
}
/**
* Extracts value from variable.
*/
public function extract(Parser $parser, Node\Variable $var, string $data): array|string
{
$value = $data;
$vals = array_filter(explode($this->sep, $data));
$options = $var->options;
switch ($options['modifier']) {
case '*':
$value = [];
foreach ($vals as $val) {
if (str_contains($val, '=')) {
[$k, $v] = explode('=', $val);
$value[$k] = $v;
} else {
$value[] = $val;
}
}
break;
case ':':
break;
default:
$value = str_contains($value, (string) $this->sep) ? $vals : $value;
}
return $this->decode($parser, $var, $value);
}
public static function createById($id)
{
if (!isset(static::$types[$id])) {
throw new \InvalidArgumentException("Invalid operator [{$id}]");
}
if (isset(static::$loaded[$id])) {
return static::$loaded[$id];
}
$op = static::$types[$id];
$class = __NAMESPACE__ . '\\' . ($op['named'] ? 'Named' : 'UnNamed');
return static::$loaded[$id] = new $class($id, $op['named'], $op['sep'], $op['empty'], $op['reserved'], $op['start'], $op['first']);
}
public static function isValid($id): bool
{
return isset(static::$types[$id]);
}
/**
* Returns the correct regex given the variable location in the URI.
*/
protected function getRegex(): string
{
return match ($this->id) {
'?', '&', '#' => self::$queryRegex,
default => self::$pathRegex,
};
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace Rize\UriTemplate\Operator;
use Rize\UriTemplate\Node;
use Rize\UriTemplate\Parser;
/**
* | 1 | {?list} ?list=red,green,blue | {name}=(?:\w+(?:,\w+?)*)*
* | 2 | {?list*} ?list=red&list=green&list=blue | {name}+=(?:{$value}+(?:{sep}{name}+={$value}*))*
* | 3 | {?keys} ?keys=semi,%3B,dot,.,comma,%2C | (same as 1)
* | 4 | {?keys*} ?semi=%3B&dot=.&comma=%2C | (same as 2)
* | 5 | {?list*} ?list[]=red&list[]=green&list[]=blue | {name[]}+=(?:{$value}+(?:{sep}{name[]}+={$value}*))*.
*/
class Named extends Abstraction
{
public function toRegex(Parser $parser, Node\Variable $var): string
{
$name = $var->name;
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$name}+=(?:{$value}+(?:{$this->sep}{$name}+={$value}*)*)"
. "|{$value}+=(?:{$value}+(?:{$this->sep}{$value}+={$value}*)*)";
break;
case ':':
$regex = "{$value}\\{0,{$options['value']}\\}";
break;
case '%':
// 5
$name .= '+(?:%5B|\[)[^=]*=';
$regex = "{$name}(?:{$value}+(?:{$this->sep}{$name}{$value}*)*)";
break;
default:
throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`");
}
} else {
// 1, 3
$regex = "{$name}=(?:{$value}+(?:,{$value}+)*)*";
}
return '(?:&)?' . $regex;
}
public function expandString(Parser $parser, Node\Variable $var, $val): string
{
$val = (string) $val;
$options = $var->options;
$result = $this->encode($parser, $var, $var->name);
// handle empty value
if ($val === '') {
return $result . $this->empty;
}
$result .= '=';
if ($options['modifier'] === ':') {
$val = mb_substr($val, 0, (int) $options['value']);
}
return $result . $this->encode($parser, $var, $val);
}
public function expandNonExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
$result = $this->encode($parser, $var, $var->name);
$result .= '=';
return $result . $this->encode($parser, $var, $val);
}
public function expandExplode(Parser $parser, Node\Variable $var, array $val): ?string
{
if (empty($val)) {
return null;
}
$list = isset($val[0]);
$data = [];
foreach ($val as $k => $v) {
// if value is a list, use `varname` as keyname, otherwise use `key` name
$key = $list ? $var->name : $k;
if ($list) {
$data[$key][] = $v;
} else {
$data[$key] = $v;
}
}
// if it's array modifier, we have to use variable name as index
// e.g. if variable name is 'query' and value is ['limit' => 1]
// then we convert it to ['query' => ['limit' => 1]]
if (!$list && $var->options['modifier'] === '%') {
$data = [$var->name => $data];
}
return $this->encodeExplodeVars($var, $data);
}
public function extract(Parser $parser, Node\Variable $var, $data): array|string
{
// get rid of optional `&` at the beginning
if ($data[0] === '&') {
$data = substr($data, 1);
}
$value = $data;
$vals = explode($this->sep, $data);
$options = $var->options;
switch ($options['modifier']) {
case '%':
parse_str($value, $query);
return $query[$var->name];
case '*':
$value = [];
foreach ($vals as $val) {
[$k, $v] = explode('=', $val);
// 2
if ($k === $var->getToken()) {
$value[] = $v;
}
// 4
else {
$value[$k] = $v;
}
}
break;
case ':':
break;
default:
// 1, 3
// remove key from value e.g. 'lang=en,th' becomes 'en,th'
$value = str_replace($var->getToken() . '=', '', $value);
$value = explode(',', $value);
if (count($value) === 1) {
$value = current($value);
}
}
return $this->decode($parser, $var, $value);
}
public function encodeExplodeVars(Node\Variable $var, $data): null|array|string
{
// http_build_query uses PHP_QUERY_RFC1738 encoding by default
// i.e. spaces are encoded as '+' (plus signs) we need to convert
// it to %20 RFC3986
$query = http_build_query($data, '', $this->sep);
$query = str_replace('+', '%20', $query);
// `%` array modifier
if ($var->options['modifier'] === '%') {
// it also uses numeric based-index by default e.g. list[] becomes list[0]
$query = preg_replace('#%5B\d+%5D#', '%5B%5D', $query);
}
// `:`, `*` modifiers
else {
// by default, http_build_query will convert array values to `a[]=1&a[]=2`
// which is different from the spec. It should be `a=1&a=2`
$query = preg_replace('#%5B\d+%5D#', '', $query);
}
// handle reserved charset
if ($this->reserved) {
$query = str_replace(
array_keys(static::$reserved_chars),
static::$reserved_chars,
$query,
);
}
return $query;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Rize\UriTemplate\Operator;
use Rize\UriTemplate\Node;
use Rize\UriTemplate\Parser;
/**
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
* | 2 | {/list*} /red/green/blue | {$value}+(?:{$sep}{$value}+)*
* | 3 | {/keys} /semi,%3B,dot,.,comma,%2C | /(\w+,?)+
* | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*.
*/
class UnNamed extends Abstraction
{
public function toRegex(Parser $parser, Node\Variable $var): string
{
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$value}+(?:{$this->sep}{$value}+)*";
break;
case ':':
$regex = $value . '{0,' . $options['value'] . '}';
break;
case '%':
throw new \InvalidArgumentException('% (array) modifier only works with Named type operators e.g. ;,?,&');
default:
throw new \InvalidArgumentException("Unknown modifier `{$options['modifier']}`");
}
} else {
// 1, 3
$regex = "{$value}*(?:,{$value}+)*";
}
return $regex;
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Rize\UriTemplate;
use Rize\UriTemplate\Node\Abstraction;
use Rize\UriTemplate\Node\Expression;
use Rize\UriTemplate\Node\Variable;
use Rize\UriTemplate\Operator\UnNamed;
class Parser
{
private const REGEX_VARNAME = '[A-z0-9.]|%[0-9a-fA-F]{2}';
/**
* Parses URI Template and returns nodes.
*
* @return Node\Abstraction[]
*/
public function parse(string $template): array
{
$parts = preg_split('#(\{[^}]+})#', $template, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$nodes = [];
foreach ($parts as $part) {
$node = $this->createNode($part);
// if current node has dot separator that requires a forward lookup
// for the previous node iff previous node's operator is UnNamed
if ($node instanceof Expression && $node->getOperator()->id === '.') {
if (count($nodes) > 0) {
$previousNode = $nodes[count($nodes) - 1];
if ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) {
$previousNode->setForwardLookupSeparator($node->getOperator()->id);
}
}
}
$nodes[] = $node;
}
return $nodes;
}
protected function createNode(string $token): Abstraction
{
// literal string
if ($token[0] !== '{') {
$node = $this->createLiteralNode($token);
} else {
// remove `{}` from expression and parse it
$node = $this->parseExpression(substr($token, 1, -1));
}
return $node;
}
protected function parseExpression(string $expression): Expression
{
$token = $expression;
$prefix = $token[0];
// not a valid operator?
if (!Operator\Abstraction::isValid($prefix)) {
// not valid chars?
if (!preg_match('#' . self::REGEX_VARNAME . '#', $token)) {
throw new \InvalidArgumentException("Invalid operator [{$prefix}] found at {$token}");
}
// default operator
//
// PHP 8.5: using null as array offset is deprecated and we no longer rely on auto-casting null to ''.
$prefix = '';
}
// remove operator prefix if exists e.g. '?'
if ($prefix) {
$token = substr($token, 1);
}
// parse variables
$vars = [];
foreach (explode(',', $token) as $var) {
$vars[] = $this->parseVariable($var);
}
return $this->createExpressionNode(
$token,
$this->createOperatorNode($prefix),
$vars,
);
}
protected function parseVariable(string $var): Variable
{
$var = trim($var);
$val = null;
$modifier = null;
// check for prefix (:) / explode (*) / array (%) modifier
if (str_contains($var, ':')) {
$modifier = ':';
[$varname, $val] = explode(':', $var);
// error checking
if (!is_numeric($val)) {
throw new \InvalidArgumentException("Value for `:` modifier must be numeric value [{$varname}:{$val}]");
}
}
switch ($last = substr($var, -1)) {
case '*':
case '%':
// there can be only 1 modifier per var
if ($modifier) {
throw new \InvalidArgumentException("Multiple modifiers per variable are not allowed [{$var}]");
}
$modifier = $last;
$var = substr($var, 0, -1);
break;
}
return $this->createVariableNode(
$var,
['modifier' => $modifier, 'value' => $val],
);
}
protected function createVariableNode($token, $options = []): Variable
{
return new Variable($token, $options);
}
protected function createExpressionNode($token, ?Operator\Abstraction $operator = null, array $vars = []): Expression
{
return new Expression($token, $operator, $vars);
}
protected function createLiteralNode(string $token): Node\Literal
{
return new Node\Literal($token);
}
protected function createOperatorNode($token): Operator\Abstraction
{
return Operator\Abstraction::createById($token);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Rize\UriTemplate;
use Rize\UriTemplate as Template;
/**
* Future compatibility.
*/
class UriTemplate extends Template {}