summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/web/src/Filter
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/web/src/Filter')
-rw-r--r--vendor/ipl/web/src/Filter/ParseException.php36
-rw-r--r--vendor/ipl/web/src/Filter/Parser.php568
-rw-r--r--vendor/ipl/web/src/Filter/QueryString.php94
-rw-r--r--vendor/ipl/web/src/Filter/Renderer.php186
4 files changed, 884 insertions, 0 deletions
diff --git a/vendor/ipl/web/src/Filter/ParseException.php b/vendor/ipl/web/src/Filter/ParseException.php
new file mode 100644
index 0000000..bcafd09
--- /dev/null
+++ b/vendor/ipl/web/src/Filter/ParseException.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace ipl\Web\Filter;
+
+use Exception;
+
+class ParseException extends Exception
+{
+ protected $char;
+
+ protected $charPos;
+
+ public function __construct($filter, $char, $charPos, $extra)
+ {
+ parent::__construct(sprintf(
+ 'Invalid filter "%s", unexpected %s at pos %d%s',
+ $filter,
+ $char,
+ $charPos,
+ $extra
+ ));
+
+ $this->char = $char;
+ $this->charPos = $charPos;
+ }
+
+ public function getChar()
+ {
+ return $this->char;
+ }
+
+ public function getCharPos()
+ {
+ return $this->charPos;
+ }
+}
diff --git a/vendor/ipl/web/src/Filter/Parser.php b/vendor/ipl/web/src/Filter/Parser.php
new file mode 100644
index 0000000..d33fd86
--- /dev/null
+++ b/vendor/ipl/web/src/Filter/Parser.php
@@ -0,0 +1,568 @@
+<?php
+
+namespace ipl\Web\Filter;
+
+use ipl\Stdlib\Events;
+use ipl\Stdlib\Filter;
+
+class Parser
+{
+ use Events;
+
+ /** @var string Emitted for every completely parsed condition */
+ const ON_CONDITION = 'on_condition';
+
+ /** @var string Emitted for every completely parsed chain */
+ const ON_CHAIN = 'on_chain';
+
+ /** @var string */
+ protected $string;
+
+ /** @var int */
+ protected $pos;
+
+ /** @var int */
+ protected $termIndex;
+
+ /** @var int */
+ protected $length;
+
+ /** @var bool Whether strict mode is enabled */
+ protected $strict = false;
+
+ /**
+ * Create a new Parser
+ *
+ * @param string $queryString The string to parse
+ */
+ public function __construct($queryString = null)
+ {
+ if ($queryString !== null) {
+ $this->setQueryString($queryString);
+ }
+ }
+
+ /**
+ * Set the query string to parse
+ *
+ * @param string $queryString
+ *
+ * @return $this
+ */
+ public function setQueryString($queryString)
+ {
+ $this->string = (string) $queryString;
+ $this->length = strlen($queryString);
+
+ return $this;
+ }
+
+ /**
+ * Set whether strict mode is enabled
+ *
+ * @param bool $strict
+ *
+ * @return $this
+ */
+ public function setStrict($strict = true)
+ {
+ $this->strict = (bool) $strict;
+
+ return $this;
+ }
+
+ /**
+ * Parse the string and derive a filter rule from it
+ *
+ * @return Filter\Rule
+ */
+ public function parse()
+ {
+ if ($this->length === 0) {
+ return Filter::all();
+ }
+
+ $this->pos = 0;
+ $this->termIndex = 0;
+
+ return $this->readFilters();
+ }
+
+ /**
+ * Read filters
+ *
+ * @param int $nestingLevel
+ * @param string $op
+ * @param array $filters
+ * @param bool $explicit
+ *
+ * @return Filter\Chain|Filter\Condition
+ * @throws ParseException
+ */
+ protected function readFilters($nestingLevel = 0, $op = null, $filters = null, $explicit = true)
+ {
+ $filters = empty($filters) ? [] : $filters;
+ $isNone = false;
+
+ while ($this->pos < $this->length) {
+ $filter = $this->readCondition();
+ $next = $this->readChar();
+
+ if ($filter === false) {
+ if ($next === '!') {
+ $isNone = true;
+ $this->termIndex++;
+ continue;
+ }
+
+ if ($op === null && ($this->strict || count($filters) > 0) && ($next === '&' || $next === '|')) {
+ $op = $next;
+ $this->termIndex++;
+ continue;
+ }
+
+ if ($next === false) {
+ // Nothing more to read
+ break;
+ }
+
+ if ($next === ')') {
+ if ($nestingLevel > 0) {
+ if (! $explicit) {
+ // The current chain was not initiated by a `(`,
+ // so this `)` does not belong to it, but still ends it
+ $this->pos--;
+ } else {
+ $this->termIndex++;
+ $next = $this->nextChar();
+ if ($next !== false && ! in_array($next, ['&', '|', ')'])) {
+ $this->pos++;
+ $this->parseError($next, 'Expected logical operator');
+ }
+ }
+
+ break;
+ }
+
+ $this->parseError($next);
+ }
+
+ if ($next === '(') {
+ $this->termIndex++;
+
+ $rule = $this->readFilters($nestingLevel + 1, $isNone ? '!' : null);
+ if ($this->strict || ! $rule instanceof Filter\Chain || ! $rule->isEmpty()) {
+ $filters[] = $rule;
+ }
+
+ $isNone = false;
+ continue;
+ }
+
+ if ($next === $op) {
+ $this->termIndex++;
+ continue;
+ }
+
+ if (in_array($next, ['&', '|'])) {
+ $this->termIndex++;
+
+ // It's a different logical operator, continue parsing based on its precedence
+ if ($op === '&') {
+ if (! empty($filters)) {
+ if (count($filters) > 1) {
+ $all = Filter::all(...$filters);
+ $filters = [$all];
+
+ $this->emit(self::ON_CHAIN, [$all]);
+ } else {
+ $filters = [$filters[0]];
+ }
+ }
+
+ $op = $next;
+ } elseif ($op === '|' || ($op === '!' && $next === '&')) {
+ $rule = $this->readFilters(
+ $nestingLevel + 1,
+ $next,
+ [array_pop($filters)],
+ false
+ );
+ if (! $rule instanceof Filter\Chain || ! $rule->isEmpty()) {
+ $filters[] = $rule;
+ }
+ }
+
+ continue;
+ }
+
+ $this->parseError($next, "$op level $nestingLevel");
+ } else {
+ if ($isNone) {
+ $isNone = false;
+ if ($filter->getValue() === true) {
+ // $filter is a result of `!column`
+ $filter->setValue(false);
+ $filters[] = $filter;
+
+ $this->emit(self::ON_CONDITION, [$filter]);
+ } else {
+ // $filter is a result of `!column=[value]`
+ $none = Filter::none($filter);
+ $filters[] = $none;
+
+ $this->emit(self::ON_CONDITION, [$filter]);
+ $this->emit(self::ON_CHAIN, [$none]);
+ }
+ } else {
+ $filters[] = $filter;
+ $this->emit(self::ON_CONDITION, [$filter]);
+ }
+
+ if ($next === false) {
+ // Got filter, nothing more to read
+ break;
+ }
+
+ if ($next === ')') {
+ if ($nestingLevel > 0) {
+ if (! $explicit) {
+ // The current chain was not initiated by a `(`,
+ // so this `)` does not belong to it, but still ends it
+ $this->pos--;
+ } else {
+ $this->termIndex++;
+ $next = $this->nextChar();
+ if ($next !== false && ! in_array($next, ['&', '|', ')'])) {
+ $this->pos++;
+ $this->parseError($next, 'Expected logical operator');
+ }
+ }
+
+ break;
+ }
+
+ $this->parseError($next);
+ }
+
+ if ($next === $op) {
+ $this->termIndex++;
+ continue;
+ }
+
+ if (in_array($next, ['&', '|'])) {
+ $this->termIndex++;
+
+ // It's a different logical operator, continue parsing based on its precedence
+ if ($op === null || $op === '&') {
+ if ($op === '&') {
+ if (count($filters) > 1) {
+ $all = Filter::all(...$filters);
+ $filters = [$all];
+
+ $this->emit(self::ON_CHAIN, [$all]);
+ } else {
+ $filters = [$filters[0]];
+ }
+ }
+
+ $op = $next;
+ } elseif ($op === '|' || ($op === '!' && $next === '&')) {
+ $rule = $this->readFilters(
+ $nestingLevel + 1,
+ $next,
+ [array_pop($filters)],
+ false
+ );
+ if (! $rule instanceof Filter\Chain || ! $rule->isEmpty()) {
+ $filters[] = $rule;
+ }
+ }
+
+ continue;
+ }
+
+ $this->parseError($next);
+ }
+ }
+
+ if ($nestingLevel === 0 && $this->pos < $this->length) {
+ $this->parseError($op, 'Did not read full filter');
+ }
+
+ switch ($op) {
+ case '&':
+ $chain = Filter::all(...$filters);
+ break;
+ case '|':
+ $chain = Filter::any(...$filters);
+ break;
+ case '!':
+ $chain = Filter::none(...$filters);
+ break;
+ case null:
+ if ((! $this->strict || $nestingLevel === 0) && ! empty($filters)) {
+ // There is only one filter expression, no chain
+ return $filters[0];
+ }
+
+ $chain = Filter::all(...$filters);
+ break;
+ default:
+ $this->parseError($op);
+ }
+
+ $this->emit(self::ON_CHAIN, [$chain]);
+
+ return $chain;
+ }
+
+ /**
+ * Read the next condition
+ *
+ * @return false|Filter\Condition
+ *
+ * @throws ParseException
+ */
+ protected function readCondition()
+ {
+ if ('' === ($column = $this->readColumn())) {
+ return false;
+ }
+
+ $columnIndex = $this->termIndex++;
+
+ foreach (['<', '>'] as $operator) {
+ if (($pos = strpos($column, $operator)) !== false) {
+ if ($this->nextChar() === '=') {
+ break;
+ }
+
+ $operatorIndex = $this->termIndex++;
+
+ $value = substr($column, $pos + 1);
+ $column = substr($column, 0, $pos);
+
+ $valueIndex = null;
+ if (ctype_digit($value)) {
+ $value = (float) $value;
+ $valueIndex = $this->termIndex++;
+ } elseif ($value) {
+ $valueIndex = $this->termIndex++;
+ }
+
+ $condition = $this->createCondition($column, $operator, $value);
+ $condition->metaData()
+ ->set('columnIndex', $columnIndex)
+ ->set('operatorIndex', $operatorIndex)
+ ->set('valueIndex', $valueIndex);
+
+ return $condition;
+ }
+ }
+
+ if (in_array($this->nextChar(), ['~', '=', '>', '<', '!'], true)) {
+ $operator = $this->readChar();
+ } else {
+ $operator = false;
+ }
+
+ if ($operator === false) {
+ $condition = Filter::equal($column, true);
+ $condition->metaData()
+ ->set('columnIndex', $columnIndex)
+ ->set('operatorIndex', null)
+ ->set('valueIndex', null);
+
+ return $condition;
+ }
+
+ $operatorIndex = $this->termIndex++;
+
+ $toFloat = false;
+ if ($operator === '=') {
+ $last = substr($column, -1);
+ if ($last === '>' || $last === '<') {
+ $operator = $last . $operator;
+ $column = substr($column, 0, -1);
+ $toFloat = true;
+ }
+ } elseif (in_array($operator, ['>', '<', '!'], true)) {
+ $toFloat = $operator === '>' || $operator === '<';
+ if (in_array($this->nextChar(), ['~', '='], true)) {
+ $operator .= $this->readChar();
+ }
+ }
+
+ $valueIndex = null;
+ $value = $this->readValue();
+ if ($toFloat && ctype_digit($value)) {
+ $value = (float) $value;
+ $valueIndex = $this->termIndex++;
+ } elseif ($value) {
+ $valueIndex = $this->termIndex++;
+ }
+
+ $condition = $this->createCondition($column, $operator, $value);
+ $condition->metaData()
+ ->set('columnIndex', $columnIndex)
+ ->set('operatorIndex', $operatorIndex)
+ ->set('valueIndex', $valueIndex);
+
+ return $condition;
+ }
+
+ /**
+ * Read the next column
+ *
+ * @return false|string false if there is none
+ */
+ protected function readColumn()
+ {
+ $str = $this->readUntil('~', '=', '(', ')', '&', '|', '>', '<', '!');
+
+ if ($str === false) {
+ return $str;
+ }
+
+ return rawurldecode($str);
+ }
+
+ /**
+ * Read the next value
+ *
+ * @return string|string[]
+ *
+ * @throws ParseException In case there's a missing `)`
+ */
+ protected function readValue()
+ {
+ if ($this->nextChar() === '(') {
+ $this->readChar();
+ $var = array_map('rawurldecode', preg_split('~\|~', $this->readUntil(')')));
+
+ if ($this->readChar() !== ')') {
+ $this->parseError(null, 'Expected ")"');
+ }
+ } else {
+ $var = rawurldecode($this->readUntil(')', '&', '|', '>', '<'));
+ }
+
+ return $var;
+ }
+
+ /**
+ * Read until any of the given chars appears
+ *
+ * @param string ...$chars
+ *
+ * @return string
+ */
+ protected function readUntil(...$chars)
+ {
+ $buffer = '';
+ while (($c = $this->readChar()) !== false) {
+ if (in_array($c, $chars, true)) {
+ $this->pos--;
+ break;
+ }
+
+ $buffer .= $c;
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Read a single character
+ *
+ * @return false|string false if there is no character left
+ */
+ protected function readChar()
+ {
+ if ($this->length > $this->pos) {
+ return $this->string[$this->pos++];
+ }
+
+ return false;
+ }
+
+ /**
+ * Look at the next character
+ *
+ * @return false|string false if there is no character left
+ */
+ protected function nextChar()
+ {
+ if ($this->length > $this->pos) {
+ return $this->string[$this->pos];
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and return a condition
+ *
+ * @param string $column
+ * @param string $operator
+ * @param mixed $value
+ *
+ * @return Filter\Condition
+ */
+ protected function createCondition($column, $operator, $value)
+ {
+ $column = trim($column);
+
+ switch ($operator) {
+ case '~':
+ return Filter::like($column, $value);
+ case '!~':
+ return Filter::unlike($column, $value);
+ case '=':
+ return Filter::equal($column, $value);
+ case '!=':
+ return Filter::unequal($column, $value);
+ case '>':
+ return Filter::greaterThan($column, $value);
+ case '>=':
+ return Filter::greaterThanOrEqual($column, $value);
+ case '<':
+ return Filter::lessThan($column, $value);
+ case '<=':
+ return Filter::lessThanOrEqual($column, $value);
+ }
+ }
+
+ /**
+ * Throw a parse exception
+ *
+ * @param string $char
+ * @param string $extraMsg
+ *
+ * @throws ParseException
+ */
+ protected function parseError($char = null, $extraMsg = null)
+ {
+ if ($extraMsg === null) {
+ $extra = '';
+ } else {
+ $extra = ': ' . $extraMsg;
+ }
+
+ if ($char === null) {
+ if ($this->pos < $this->length) {
+ $char = $this->string[$this->pos];
+ } else {
+ $char = $this->string[--$this->pos];
+ }
+ }
+
+ throw new ParseException(
+ $this->string,
+ $char,
+ $this->pos,
+ $extra
+ );
+ }
+}
diff --git a/vendor/ipl/web/src/Filter/QueryString.php b/vendor/ipl/web/src/Filter/QueryString.php
new file mode 100644
index 0000000..e1bb533
--- /dev/null
+++ b/vendor/ipl/web/src/Filter/QueryString.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace ipl\Web\Filter;
+
+use InvalidArgumentException;
+use ipl\Stdlib\Filter;
+
+final class QueryString
+{
+ /** @var string Emitted for every completely parsed condition */
+ const ON_CONDITION = Parser::ON_CONDITION;
+
+ /** @var string Emitted for every completely parsed chain */
+ const ON_CHAIN = Parser::ON_CHAIN;
+
+ /**
+ * This class is only a factory / helper
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Derive a rule from the given query string
+ *
+ * @param string $string
+ *
+ * @return Parser
+ */
+ public static function fromString($string)
+ {
+ return new Parser($string);
+ }
+
+ /**
+ * Derive a rule from the given query string
+ *
+ * @param string $string
+ *
+ * @return Filter\Rule
+ */
+ public static function parse($string)
+ {
+ return (new Parser($string))->parse();
+ }
+
+ /**
+ * Assemble a query string for the given rule
+ *
+ * @param Filter\Rule $rule
+ *
+ * @return string
+ */
+ public static function render(Filter\Rule $rule)
+ {
+ return (new Renderer($rule))->render();
+ }
+
+ /**
+ * Get the symbol associated with the given rule
+ *
+ * @param Filter\Rule $rule
+ *
+ * @return string
+ */
+ public static function getRuleSymbol(Filter\Rule $rule)
+ {
+ switch (true) {
+ case $rule instanceof Filter\Unlike:
+ return '!~';
+ case $rule instanceof Filter\Unequal:
+ return '!=';
+ case $rule instanceof Filter\Like:
+ return '~';
+ case $rule instanceof Filter\Equal:
+ return '=';
+ case $rule instanceof Filter\GreaterThan:
+ return '>';
+ case $rule instanceof Filter\LessThan:
+ return '<';
+ case $rule instanceof Filter\GreaterThanOrEqual:
+ return '>=';
+ case $rule instanceof Filter\LessThanOrEqual:
+ return '<=';
+ case $rule instanceof Filter\All:
+ return '&';
+ case $rule instanceof Filter\Any:
+ case $rule instanceof Filter\None:
+ return '|';
+ default:
+ throw new InvalidArgumentException('Unknown rule type provided');
+ }
+ }
+}
diff --git a/vendor/ipl/web/src/Filter/Renderer.php b/vendor/ipl/web/src/Filter/Renderer.php
new file mode 100644
index 0000000..513470e
--- /dev/null
+++ b/vendor/ipl/web/src/Filter/Renderer.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace ipl\Web\Filter;
+
+use ipl\Stdlib\Filter;
+
+class Renderer
+{
+ /** @var Filter\Rule */
+ protected $filter;
+
+ /** @var string */
+ protected $string;
+
+ /** @var bool Whether strict mode is enabled */
+ protected $strict = false;
+
+ /**
+ * Create a new filter Renderer
+ *
+ * @param Filter\Rule $filter
+ */
+ public function __construct(Filter\Rule $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ /**
+ * Set whether strict mode is enabled
+ *
+ * @param bool $strict
+ *
+ * @return $this
+ */
+ public function setStrict($strict = true)
+ {
+ $this->strict = (bool) $strict;
+
+ return $this;
+ }
+
+ /**
+ * Assemble and return the filter as query string
+ *
+ * @return string
+ */
+ public function render()
+ {
+ if ($this->string !== null) {
+ return $this->string;
+ }
+
+ $this->string = '';
+ $filter = $this->filter;
+
+ if ($filter instanceof Filter\Chain) {
+ $this->renderChain($filter, $this->strict);
+ } else {
+ /** @var Filter\Condition $filter */
+ $this->renderCondition($filter);
+ }
+
+ return $this->string;
+ }
+
+ /**
+ * Assemble the given filter Chain
+ *
+ * @param Filter\Chain $chain
+ * @param bool $wrap
+ *
+ * @return void
+ */
+ protected function renderChain(Filter\Chain $chain, $wrap = false)
+ {
+ if (! $this->strict && $chain->isEmpty()) {
+ return;
+ }
+
+ $chainOperator = null;
+ switch (true) {
+ case $chain instanceof Filter\All:
+ $chainOperator = '&';
+ break;
+ case $chain instanceof Filter\None:
+ $this->string .= '!';
+
+ // Force wrap, it may be the root node
+ if (! $wrap) {
+ if ($chain->count() > 1) {
+ $wrap = true;
+ } else {
+ $iterator = $chain->getIterator();
+ $wrap = $iterator->current() instanceof Filter\None;
+ }
+ }
+
+ // None shares the operator with Any
+ case $chain instanceof Filter\Any:
+ $chainOperator = '|';
+ break;
+ }
+
+ if ($wrap) {
+ $this->string .= '(';
+ }
+
+ foreach ($chain as $rule) {
+ if ($rule instanceof Filter\Chain) {
+ $this->renderChain($rule, $this->strict || $rule->count() > 1);
+ } else {
+ /** @var Filter\Condition $rule */
+ $this->renderCondition($rule);
+ }
+
+ $this->string .= $chainOperator;
+ }
+
+ if (! $chain->isEmpty() && (! $this->strict || ! ($chain instanceof Filter\Any && $chain->count() === 1))) {
+ // Remove redundant chain operator added last
+ $this->string = substr($this->string, 0, -1);
+ } elseif ($chain->isEmpty() && $chain instanceof Filter\Any) {
+ // If the chain is empty and strict mode is on, we need a
+ // chain operator to designate it's an OR, not an AND
+ $this->string .= $chainOperator;
+ }
+
+ if ($wrap) {
+ $this->string .= ')';
+ }
+ }
+
+ /**
+ * Assemble the given filter Condition
+ *
+ * @param Filter\Condition $condition
+ *
+ * @return void
+ */
+ protected function renderCondition(Filter\Condition $condition)
+ {
+ $value = $condition->getValue();
+ if (is_bool($value) && ! $value) {
+ $this->string .= '!';
+ }
+
+ $this->string .= rawurlencode($condition->getColumn());
+
+ if (is_bool($value)) {
+ return;
+ }
+
+ switch (true) {
+ case $condition instanceof Filter\Unlike:
+ $this->string .= '!~';
+ break;
+ case $condition instanceof Filter\Unequal:
+ $this->string .= '!=';
+ break;
+ case $condition instanceof Filter\Like:
+ $this->string .= '~';
+ break;
+ case $condition instanceof Filter\Equal:
+ $this->string .= '=';
+ break;
+ case $condition instanceof Filter\GreaterThan:
+ $this->string .= rawurlencode('>');
+ break;
+ case $condition instanceof Filter\LessThan:
+ $this->string .= rawurlencode('<');
+ break;
+ case $condition instanceof Filter\GreaterThanOrEqual:
+ $this->string .= rawurlencode('>') . '=';
+ break;
+ case $condition instanceof Filter\LessThanOrEqual:
+ $this->string .= rawurlencode('<') . '=';
+ break;
+ }
+
+ if (is_array($value)) {
+ $this->string .= '(' . join('|', array_map('rawurlencode', $value)) . ')';
+ } elseif ($value !== null) {
+ $this->string .= rawurlencode($value);
+ }
+ }
+}