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:
Thomas Gelf 2014-06-06 05:58:42 +00:00
parent d44a87717d
commit f438cb30e1
8 changed files with 530 additions and 0 deletions

View File

@ -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)
{
}
}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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 . ')';
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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';