Data\Filter: initial commit basic implementation
Basic operators are there, still missing: subclassed "where" to distinct comparison operators like greater/less than on a class level. refs #6418
This commit is contained in:
parent
d44a87717d
commit
f438cb30e1
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Filter
|
||||
*
|
||||
* Base class for filters (why?) and factory for the different FilterOperators
|
||||
*/
|
||||
class Filter
|
||||
{
|
||||
protected $id = '1';
|
||||
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getById($id)
|
||||
{
|
||||
if ($id === $this->getId()) {
|
||||
return $this;
|
||||
}
|
||||
throw new Exception(sprintf(
|
||||
'Trying to get invalid filter index "%s" from "%s"', $id, $this
|
||||
));
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function hasId($id)
|
||||
{
|
||||
if ($id === $this->getId()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where Filter factory
|
||||
*
|
||||
* @param string $col Column to be filtered
|
||||
* @param string $filter Filter expression
|
||||
*
|
||||
* @throws FilterException
|
||||
* @return FilterWhere
|
||||
*/
|
||||
public static function where($col, $filter)
|
||||
{
|
||||
return new FilterWhere($col, $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Or FilterOperator factory
|
||||
*
|
||||
* @param Filter $filter,... Unlimited optional list of Filters
|
||||
*
|
||||
* @return FilterOr
|
||||
*/
|
||||
public static function matchAny()
|
||||
{
|
||||
return new FilterOr(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Or FilterOperator factory
|
||||
*
|
||||
* @param Filter $filter,... Unlimited optional list of Filters
|
||||
*
|
||||
* @return FilterAnd
|
||||
*/
|
||||
public static function matchAll()
|
||||
{
|
||||
return new FilterAnd(func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* FilterNot factory, negates the given filter
|
||||
*
|
||||
* @param Filter $filter Filter to be negated
|
||||
*
|
||||
* @return FilterNot
|
||||
*/
|
||||
public static function not(Filter $filter)
|
||||
{
|
||||
return new FilterNot(array($filter)); // ??
|
||||
}
|
||||
|
||||
/**
|
||||
* Create filter from queryString
|
||||
*
|
||||
* This is still pretty basic, need improvement
|
||||
*/
|
||||
public static function fromQueryString($query)
|
||||
{
|
||||
$query = rawurldecode($query);
|
||||
$parts = preg_split('~&~', $query, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$filters = Filter::matchAll();
|
||||
foreach ($parts as $part) {
|
||||
self::parseQueryStringPart($part, $filters);
|
||||
}
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query string part
|
||||
*
|
||||
*/
|
||||
protected static function parseQueryStringPart($part, & $filters)
|
||||
{
|
||||
$negations = 0;
|
||||
|
||||
if (strpos($part, '=') === false) {
|
||||
|
||||
$key = rawurldecode($part);
|
||||
|
||||
while (substr($key, 0, 1) === '!') {
|
||||
if (strlen($key) < 2) {
|
||||
throw new FilterException(
|
||||
sprintf('Got invalid filter part: "%s"', $part)
|
||||
);
|
||||
}
|
||||
$key = substr($key, 1);
|
||||
$negations++;
|
||||
}
|
||||
|
||||
$filter = Filter::where($key, true);
|
||||
|
||||
} else {
|
||||
list($key, $val) = preg_split('/=/', $part, 2);
|
||||
$key = rawurldecode($key);
|
||||
$val = rawurldecode($val);
|
||||
|
||||
while (substr($key, 0, 1) === '!') {
|
||||
if (strlen($key) < 2) {
|
||||
throw new FilterException(
|
||||
sprintf('Got invalid filter part: "%s"', $part)
|
||||
);
|
||||
}
|
||||
$key = substr($key, 1);
|
||||
$negations++;
|
||||
}
|
||||
|
||||
while (substr($key, -1) === '!') {
|
||||
if (strlen($key) < 2) {
|
||||
throw new FilterException(
|
||||
sprintf('Got invalid filter part: "%s"', $part)
|
||||
);
|
||||
}
|
||||
$key = substr($key, 0, -1);
|
||||
$negations++;
|
||||
}
|
||||
|
||||
if (strpos($val, '|') !== false) {
|
||||
$vals = preg_split('/\|/', $val, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$filter = Filter::matchAny();
|
||||
foreach ($vals as $val) {
|
||||
$filter->addFilter(Filter::where($key, $val));
|
||||
}
|
||||
} else {
|
||||
$filter = Filter::where($key, $val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($negations % 2 === 0) {
|
||||
$filters->addFilter($filter);
|
||||
} else {
|
||||
$filters->addFilter(Filter::not($filter));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need a new Querystring-Parser
|
||||
*
|
||||
* Still TBD, should be able to read such syntax:
|
||||
* (host_name=test&(service=ping|(service=http&host=*net*)))
|
||||
*/
|
||||
protected static function consumeStringUnless(& $string, $stop)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
/**
|
||||
* Filter list AND
|
||||
*
|
||||
* Binary AND, all contained filters must succeed
|
||||
*/
|
||||
class FilterAnd extends FilterOperator
|
||||
{
|
||||
protected $operatorName = 'AND';
|
||||
|
||||
protected $operatorSymbol = '&';
|
||||
|
||||
/**
|
||||
* Whether the given row object matches this filter
|
||||
*
|
||||
* @object $row
|
||||
* @return boolean
|
||||
*/
|
||||
public function matches($row)
|
||||
{
|
||||
foreach ($this->filters as $filter) {
|
||||
if (! $filter->matches($row)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Filter Exception Class
|
||||
*
|
||||
* Filter Exceptions should be thrown on filter parse errors or similar
|
||||
*/
|
||||
class FilterException extends Exception {}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterNot extends FilterOperator
|
||||
{
|
||||
protected $operatorName = 'NOT';
|
||||
|
||||
protected $operatorSymbol = '!'; // BULLSHIT
|
||||
|
||||
// TODO: Max count 1 or autocreate sub-and?
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
foreach ($this->filters() as $filter) {
|
||||
if ($filter->matches($row)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toQueryString()
|
||||
{
|
||||
$parts = array();
|
||||
if (empty($this->filters)) {
|
||||
return '';
|
||||
}
|
||||
foreach ($this->filters() as $filter) {
|
||||
$parts[] = $filter->toQueryString();
|
||||
}
|
||||
if (count($parts) === 1) {
|
||||
return '!' . $parts[0];
|
||||
} else {
|
||||
return '!(' . implode('&', $parts) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$sub = Filter::matchAll();
|
||||
var_dump($this->filters());
|
||||
foreach ($this->filters() as $f) {
|
||||
$sub->addFilter($f);
|
||||
}
|
||||
return '! (' . $sub . ')';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
/**
|
||||
* FilterOperator
|
||||
*
|
||||
* A FilterOperator contains a list ...
|
||||
*/
|
||||
abstract class FilterOperator extends Filter
|
||||
{
|
||||
protected $filters = array();
|
||||
|
||||
protected $operatorName;
|
||||
|
||||
protected $operatorSymbol;
|
||||
|
||||
public function hasId($id)
|
||||
{
|
||||
foreach ($this->filters() as $filter) {
|
||||
if ($filter->hasId($id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return parent::hasId($id);
|
||||
}
|
||||
|
||||
public function getById($id)
|
||||
{
|
||||
foreach ($this->filters() as $filter) {
|
||||
if ($filter->hasId($id)) {
|
||||
return $filter->getById($id);
|
||||
}
|
||||
}
|
||||
return parent::getById($id);
|
||||
}
|
||||
|
||||
public function removeId($id)
|
||||
{
|
||||
if ($id === $this->getId()) {
|
||||
$this->filters = array();
|
||||
return $this;
|
||||
}
|
||||
$remove = null;
|
||||
foreach ($this->filters as $key => $filter) {
|
||||
if ($filter->getId() === $id) {
|
||||
$remove = $key;
|
||||
} elseif ($filter instanceof FilterOperator) {
|
||||
$filter->removeId($id);
|
||||
}
|
||||
}
|
||||
if ($remove !== null) {
|
||||
unset($this->filters[$remove]);
|
||||
$this->filters = array_values($this->filters);
|
||||
}
|
||||
$this->refreshChildIds();
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function refreshChildIds()
|
||||
{
|
||||
$i = 0;
|
||||
$id = $this->getId();
|
||||
foreach ($this->filters as $filter) {
|
||||
$i++;
|
||||
$filter->setId($id . '-' . $i);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setId($id)
|
||||
{
|
||||
return parent::setId($id)->refreshChildIds();
|
||||
}
|
||||
|
||||
public function getOperatorName()
|
||||
{
|
||||
return $this->operatorName;
|
||||
}
|
||||
|
||||
public function getOperatorSymbol()
|
||||
{
|
||||
return $this->operatorSymbol;
|
||||
}
|
||||
|
||||
public function toQueryString()
|
||||
{
|
||||
$parts = array();
|
||||
if (empty($this->filters)) {
|
||||
return '';
|
||||
}
|
||||
foreach ($this->filters() as $filter) {
|
||||
$parts[] = $filter->toQueryString();
|
||||
}
|
||||
|
||||
// TODO: getLevel??
|
||||
if (strpos($this->getId(), '-')) {
|
||||
return '(' . implode($this->getOperatorSymbol(), $parts) . ')';
|
||||
} else {
|
||||
return implode($this->getOperatorSymbol(), $parts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get simple string representation
|
||||
*
|
||||
* Useful for debugging only
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
if (empty($this->filters)) {
|
||||
return '';
|
||||
}
|
||||
$parts = array();
|
||||
foreach ($this->filters as $filter) {
|
||||
if ($filter instanceof FilterOperator) {
|
||||
$parts[] = '(' . $filter . ')';
|
||||
} else {
|
||||
$parts[] = (string) $filter;
|
||||
}
|
||||
}
|
||||
$op = ' ' . $this->getOperatorSymbol() . ' ';
|
||||
return implode($op, $parts);
|
||||
}
|
||||
|
||||
public function __construct($filters = array())
|
||||
{
|
||||
foreach ($filters as $filter) {
|
||||
$this->addFilter($filter);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->filters);
|
||||
}
|
||||
|
||||
public function addFilter(Filter $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
$filter->setId($this->getId() . '-' . (count($this->filters)));
|
||||
}
|
||||
|
||||
public function &filters()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->filters as & $filter) {
|
||||
$filter = clone $filter;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterOr extends FilterOperator
|
||||
{
|
||||
protected $operatorName = 'OR';
|
||||
|
||||
protected $operatorSymbol = '|';
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
foreach ($this->filters as $filter) {
|
||||
if ($filter->matches($row)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Icinga\Data\Filter;
|
||||
|
||||
class FilterWhere extends Filter
|
||||
{
|
||||
protected $column;
|
||||
protected $expression;
|
||||
|
||||
public function __construct($column, $expression)
|
||||
{
|
||||
$this->column = $column;
|
||||
$this->expression = $expression;
|
||||
}
|
||||
|
||||
public function getColumn()
|
||||
{
|
||||
return $this->column;
|
||||
}
|
||||
|
||||
public function setColumn($column)
|
||||
{
|
||||
$this->column = $column;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpression()
|
||||
{
|
||||
return $this->expression;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
if (is_array($this->expression)) {
|
||||
return $this->column . ' = ( ' . implode(' | ', $this->expression) . ' )';
|
||||
} else {
|
||||
return $this->column . ' = ' . $this->expression;
|
||||
}
|
||||
}
|
||||
|
||||
public function toQueryString()
|
||||
{
|
||||
if (is_array($this->expression)) {
|
||||
return $this->column . '=' . implode('|', $this->expression);
|
||||
} else {
|
||||
return $this->column . '=' . $this->expression;
|
||||
}
|
||||
}
|
||||
|
||||
public function matches($row)
|
||||
{
|
||||
if (is_array($this->expression)) {
|
||||
return in_array($row->{$this->column}, $this->expression);
|
||||
} elseif (strpos($this->expression, '*') === false) {
|
||||
return (string) $row->{$this->column} === (string) $this->expression;
|
||||
} else {
|
||||
$parts = preg_split('~\*~', $this->expression);
|
||||
foreach ($parts as & $part) {
|
||||
$part = preg_quote($part);
|
||||
}
|
||||
$pattern = '/^' . implode('.*', $parts) . '$/';
|
||||
return (bool) preg_match($pattern, $row->{$this->column});
|
||||
}
|
||||
|
||||
foreach ($this->filters as $filter) {
|
||||
if (! $filter->matches($row)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -158,6 +158,8 @@ class FilterTest extends BaseTestCase
|
|||
}
|
||||
|
||||
*/
|
||||
|
||||
// Playing around to get ready for new queryString parser
|
||||
public function testFromQueryString()
|
||||
{
|
||||
$string = 'host_name=localhost&(service_state=1|service_state=2|service_state=3)&service_problem=1';
|
||||
|
|
Loading…
Reference in New Issue