Merge pull request #775 from simonwelsh/exclude-filter

Exclude filter
This commit is contained in:
Sam Minnée 2012-10-09 15:58:19 -07:00
commit 7023669754
16 changed files with 652 additions and 152 deletions

View File

@ -205,13 +205,18 @@ This would be equivalent to a SQL query of
### Search Filter Modifiers ### Search Filter Modifiers
The where clauses showcased in the previous two sections (filter and exclude) specify case-insensitive exact The where clauses showcased in the previous two sections (filter and exclude) specify exact
matches by default. However, there are a number of suffixes that you can put on field names to change this matches by default. However, there are a number of suffixes that you can put on field names to change this
behaviour `":StartsWith"`, `":EndsWith"`, `":PartialMatch"`, `":GreaterThan"`, `":LessThan"`, `":Negation"`. behaviour `":StartsWith"`, `":EndsWith"`, `":PartialMatch"`, `":GreaterThan"`, `":LessThan"`, `":Negation"`.
Each of these suffixes is represented in the ORM as a subclass of `[api:SearchFilter]`. Developers can define Each of these suffixes is represented in the ORM as a subclass of `[api:SearchFilter]`. Developers can define
their own SearchFilters if needing to extend the ORM filter and exclude behaviours. their own SearchFilters if needing to extend the ORM filter and exclude behaviours.
These suffixes can also take modifiers themselves. The modifiers currently supported are `":not"`, `":nocase"`
and `":case"`. These negate the filter, make it case-insensitive and make it case-sensitive respectively. The
default comparison uses the database's default. For MySQL and MSSQL, this is case-insensitive. For PostgreSQL,
this is case-sensitive.
The following is a query which will return everyone whose first name doesn't start with S, who has logged in The following is a query which will return everyone whose first name doesn't start with S, who has logged in
since 1/1/2011. since 1/1/2011.

View File

@ -374,38 +374,15 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* Return a new instance of the list with an added filter * Return a new instance of the list with an added filter
*/ */
public function addFilter($filterArray) { public function addFilter($filterArray) {
$SQL_Statements = array();
foreach($filterArray as $field => $value) { foreach($filterArray as $field => $value) {
if(is_array($value)) { $fieldArgs = explode(':', $field);
$customQuery = 'IN (\''.implode('\',\'',Convert::raw2sql($value)).'\')'; $field = array_shift($fieldArgs);
} else { $filterType = array_shift($fieldArgs);
$customQuery = '= \''.Convert::raw2sql($value).'\''; $modifiers = $fieldArgs;
} $this->applyFilterContext($field, $filterType, $modifiers, $value);
if(stristr($field,':')) {
$fieldArgs = explode(':',$field);
$field = array_shift($fieldArgs);
foreach($fieldArgs as $fieldArg){
$comparisor = $this->applyFilterContext($field, $fieldArg, $value);
}
} else {
if($field == 'ID') {
$field = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass));
} else {
$field = '"' . Convert::raw2sql($field) . '"';
}
$SQL_Statements[] = $field . ' ' . $customQuery;
}
} }
if(!count($SQL_Statements)) return $this; return $this;
return $this->alterDataQuery_30(function($query) use ($SQL_Statements){
foreach($SQL_Statements as $SQL_Statement){
$query->where($SQL_Statement);
}
});
} }
/** /**
@ -459,16 +436,22 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* *
* @param string $field - the fieldname in the db * @param string $field - the fieldname in the db
* @param string $comparisators - example StartsWith, relates to a filtercontext * @param string $comparisators - example StartsWith, relates to a filtercontext
* @param array $modifiers - Modifiers to pass to the filter, ie not,nocase
* @param string $value - the value that the filtercontext will use for matching * @param string $value - the value that the filtercontext will use for matching
* @todo Deprecated SearchContexts and pull their functionality into the core of the ORM * @todo Deprecated SearchContexts and pull their functionality into the core of the ORM
*/ */
private function applyFilterContext($field, $comparisators, $value) { private function applyFilterContext($field, $comparisators, $modifiers, $value) {
$t = singleton($this->dataClass())->dbObject($field); $t = singleton($this->dataClass())->dbObject($field);
$className = "{$comparisators}Filter"; if($comparisators) {
if(!class_exists($className)){ $className = "{$comparisators}Filter";
throw new InvalidArgumentException('There are no '.$comparisators.' comparisator'); } else {
$className = 'ExactMatchFilter';
} }
$t = new $className($field,$value); if(!class_exists($className)){
$className = 'ExactMatchFilter';
array_unshift($modifiers, $comparisators);
}
$t = new $className($field, $value, $modifiers);
$t->apply($this->dataQuery()); $t->apply($this->dataQuery());
} }
@ -500,25 +483,29 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()'); throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()');
} }
$SQL_Statements = array(); return $this->alterDataQuery(function($query, $list) use ($whereArguments) {
foreach($whereArguments as $fieldName => $value) { $subquery = $query->disjunctiveGroup();
if($fieldName == 'ID') {
$fieldName = sprintf('"%s"."ID"', ClassInfo::baseDataClass($this->dataClass)); foreach($whereArguments as $field => $value) {
} else { $fieldArgs = explode(':', $field);
$fieldName = '"' . Convert::raw2sql($fieldName) . '"'; $field = array_shift($fieldArgs);
$filterType = array_shift($fieldArgs);
$modifiers = $fieldArgs;
// This is here since PHP 5.3 can't call protected/private methods in a closure.
$t = singleton($list->dataClass())->dbObject($field);
if($filterType) {
$className = "{$filterType}Filter";
} else {
$className = 'ExactMatchFilter';
}
if(!class_exists($className)){
$className = 'ExactMatchFilter';
array_unshift($modifiers, $filterType);
}
$t = new $className($field, $value, $modifiers);
$t->exclude($subquery);
} }
if(is_array($value)){
$SQL_Statements[] = ($fieldName . ' NOT IN (\''.implode('\',\'', Convert::raw2sql($value)).'\')');
} else {
$SQL_Statements[] = ($fieldName . ' != \''.Convert::raw2sql($value).'\'');
}
}
if(!count($SQL_Statements)) return $this;
return $this->alterDataQuery_30(function($query) use ($SQL_Statements){
$query->whereAny($SQL_Statements);
}); });
} }

View File

@ -17,23 +17,102 @@
* @subpackage search * @subpackage search
*/ */
class EndsWithFilter extends SearchFilter { class EndsWithFilter extends SearchFilter {
protected function comparison($exclude = false) {
$modifiers = $this->getModifiers();
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
throw new InvalidArgumentException(
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
}
if(DB::getConn() instanceof PostgreSQLDatabase) {
if(in_array('case', $modifiers)) {
$comparison = 'LIKE';
} else {
$comparison = 'ILIKE';
}
} elseif(in_array('case', $modifiers)) {
$comparison = 'LIKE BINARY';
} else {
$comparison = 'LIKE';
}
if($exclude) {
$comparison = 'NOT ' . $comparison;
}
return $comparison;
}
/** /**
* Applies a match on the trailing characters of a field value. * Applies a match on the trailing characters of a field value.
* *
* @return unknown * @return DataQuery
*/ */
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
return $query->where(sprintf( return $query->where(sprintf(
"%s %s '%%%s'", "%s %s '%%%s'",
$this->getDbName(), $this->getDbName(),
(DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', $this->comparison(false),
Convert::raw2sql($this->getValue()) Convert::raw2sql($this->getValue())
)); ));
} }
/**
* Applies a match on the trailing characters of a field value.
* Matches against one of the many values.
*
* @return DataQuery
*/
protected function applyMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$connectives = array();
foreach($this->getValue() as $value) {
$connectives[] = sprintf(
"%s %s '%%%s'",
$this->getDbName(),
$this->comparison(false),
Convert::raw2sql($value)
);
}
$whereClause = implode(' OR ', $connectives);
return $query->where($whereClause);
}
/**
* Excludes a match on the trailing characters of a field value.
*
* @return DataQuery
*/
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s NOT %s '%%%s'",
$this->getDbName(),
$this->comparison(true),
Convert::raw2sql($this->getValue())
));
}
/**
* Excludes a match on the trailing characters of a field value.
* Excludes a field if it matches any of the values.
*
* @return DataQuery
*/
protected function excludeMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$connectives = array();
foreach($this->getValue() as $value) {
$connectives[] = sprintf(
"%s NOT %s '%%%s'",
$this->getDbName(),
$this->comparison(true),
Convert::raw2sql($value)
);
}
$whereClause = implode(' AND ', $connectives);
return $query->where($whereClause);
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -14,22 +14,134 @@
* @subpackage search * @subpackage search
*/ */
class ExactMatchFilter extends SearchFilter { class ExactMatchFilter extends SearchFilter {
protected function comparison($exclude = false) {
$modifiers = $this->getModifiers();
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
throw new InvalidArgumentException(
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
}
if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) {
if($exclude) {
return '!=';
} else {
return '=';
}
} elseif(DB::getConn() instanceof PostgreSQLDatabase) {
if(in_array('case', $modifiers)) {
$comparison = 'LIKE';
} else {
$comparison = 'ILIKE';
}
} elseif(in_array('case', $modifiers)) {
$comparison = 'LIKE BINARY';
} else {
$comparison = 'LIKE';
}
if($exclude) {
$comparison = 'NOT ' . $comparison;
}
return $comparison;
}
/** /**
* Applies an exact match (equals) on a field value. * Applies an exact match (equals) on a field value.
* *
* @return unknown * @return DataQuery
*/ */
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
return $query->where(sprintf( return $query->where(sprintf(
"%s = '%s'", "%s %s '%s'",
$this->getDbName(), $this->getDbName(),
$this->comparison(false),
Convert::raw2sql($this->getValue()) Convert::raw2sql($this->getValue())
)); ));
} }
/**
* Applies an exact match (equals) on a field value against multiple
* possible values.
*
* @return DataQuery
*/
protected function applyMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$values = array();
foreach($this->getValue() as $value) {
$values[] = Convert::raw2sql($value);
}
if($this->comparison(false) == '=') {
// Neither :case nor :nocase
$valueStr = "'" . implode("', '", $values) . "'";
return $query->where(sprintf(
'%s IN (%s)',
$this->getDbName(),
$valueStr
));
} else {
foreach($values as &$v) {
$v = sprintf(
"%s %s '%s'",
$this->getDbName(),
$this->comparison(false),
$v
);
}
$where = implode(' OR ', $values);
return $query->where($where);
}
}
/**
* Excludes an exact match (equals) on a field value.
*
* @return DataQuery
*/
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s %s '%s'",
$this->getDbName(),
$this->comparison(true),
Convert::raw2sql($this->getValue())
));
}
/**
* Excludes an exact match (equals) on a field value against multiple
* possible values.
*
* @return DataQuery
*/
protected function excludeMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$values = array();
foreach($this->getValue() as $value) {
$values[] = Convert::raw2sql($value);
}
if($this->comparison(false) == '=') {
// Neither :case nor :nocase
$valueStr = "'" . implode("', '", $values) . "'";
return $query->where(sprintf(
'%s NOT IN (%s)',
$this->getDbName(),
$valueStr
));
} else {
foreach($values as &$v) {
$v = sprintf(
"%s %s '%s'",
$this->getDbName(),
$this->comparison(true),
$v
);
}
$where = implode(' OR ', $values);
return $query->where($where);
}
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -7,46 +7,46 @@
/** /**
* Checks if a value is in a given set. * Checks if a value is in a given set.
* SQL syntax used: Column IN ('val1','val2') * SQL syntax used: Column IN ('val1','val2')
* @deprecated 3.1 Use ExactMatchFilter instead
* *
* @todo Add negation (NOT IN)6
* @package framework * @package framework
* @subpackage search * @subpackage search
*/ */
class ExactMatchMultiFilter extends SearchFilter { class ExactMatchMultiFilter extends SearchFilter {
function __construct($fullName, $value = false, array $modifiers = array()) {
Deprecation::notice('3.1', 'Use ExactMatchFilter instead.');
parent::__construct($fullName, $value, $modifiers);
}
public function apply(DataQuery $query) { public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); if (!is_array($this->getValue())) {
// hack
// PREVIOUS $values = explode(',',$this->getValue());
$values = array();
if (is_string($this->getValue())) {
$values = explode(',',$this->getValue()); $values = explode(',',$this->getValue());
} else {
$values = $this->getValue();
} }
else { $filter = new ExactMatchFilter($this->getFullName(), $values, $this->getModifiers());
foreach($this->getValue() as $v) { return $filter->apply($query);
$values[] = $v; }
}
protected function applyOne(DataQuery $query) {
/* NO OP */
}
public function exclude(DataQuery $query) {
if (!is_array($this->getValue())) {
$values = explode(',',$this->getValue());
} else {
$values = $this->getValue();
} }
$filter = new ExactMatchFilter($this->getFullName(), $values, $this->getModifiers());
return $filter->exclude($query);
if(! $values) return false; }
for($i = 0; $i < count($values); $i++) {
if(! is_numeric($values[$i])) { protected function excludeOne(DataQuery $query) {
// @todo Fix string replacement to only replace leading and tailing quotes /* NO OP */
$values[$i] = str_replace("'", '', $values[$i]);
$values[$i] = Convert::raw2sql($values[$i]);
}
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where(sprintf(
"%s IN (%s)",
$this->getDbName(),
$SQL_valueStr
));
} }
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -27,7 +27,7 @@
*/ */
class FulltextFilter extends SearchFilter { class FulltextFilter extends SearchFilter {
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
return $query->where(sprintf( return $query->where(sprintf(
"MATCH (%s) AGAINST ('%s')", "MATCH (%s) AGAINST ('%s')",
$this->getDbName(), $this->getDbName(),
@ -35,7 +35,15 @@ class FulltextFilter extends SearchFilter {
)); ));
} }
protected function excludeOne(DataQuery $query) {
return $query->where(sprintf(
"NOT MATCH (%s) AGAINST ('%s')",
$this->getDbName(),
Convert::raw2sql($this->getValue())
));
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -10,9 +10,9 @@
class GreaterThanFilter extends SearchFilter { class GreaterThanFilter extends SearchFilter {
/** /**
* @return $query * @return DataQuery
*/ */
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
$value = $this->getDbFormattedValue(); $value = $this->getDbFormattedValue();
@ -21,8 +21,21 @@ class GreaterThanFilter extends SearchFilter {
return $query->where($filter); return $query->where($filter);
} }
/**
* @return DataQuery
*/
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$value = $this->getDbFormattedValue();
if(is_numeric($value)) $filter = sprintf("%s <= %s", $this->getDbName(), Convert::raw2sql($value));
else $filter = sprintf("%s <= '%s'", $this->getDbName(), Convert::raw2sql($value));
return $query->where($filter);
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -10,9 +10,9 @@
class LessThanFilter extends SearchFilter { class LessThanFilter extends SearchFilter {
/** /**
* @return $query * @return DataQuery
*/ */
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
$value = $this->getDbFormattedValue(); $value = $this->getDbFormattedValue();
@ -21,8 +21,21 @@ class LessThanFilter extends SearchFilter {
return $query->where($filter); return $query->where($filter);
} }
/**
* @return DataQuery
*/
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$value = $this->getDbFormattedValue();
if(is_numeric($value)) $filter = sprintf("%s >= %s", $this->getDbName(), Convert::raw2sql($value));
else $filter = sprintf("%s >= '%s'", $this->getDbName(), Convert::raw2sql($value));
return $query->where($filter);
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -2,19 +2,33 @@
/** /**
* Matches on rows where the field is not equal to the given value. * Matches on rows where the field is not equal to the given value.
* *
* @deprecated 3.1 Use ExactMatchFilter:not instead
* @package framework * @package framework
* @subpackage search * @subpackage search
*/ */
class NegationFilter extends SearchFilter { class NegationFilter extends SearchFilter {
function __construct($fullName, $value = false, array $modifiers = array()) {
public function apply(DataQuery $query) { Deprecation::notice('3.1', 'Use ExactMatchFilter:not instead.');
$this->model = $query->applyRelation($this->relation); $modifiers[] = 'not';
return $query->where(sprintf( parent::__construct($fullName, $value, $modifiers);
"%s != '%s'",
$this->getDbName(),
Convert::raw2sql($this->getValue())
));
} }
public function apply(DataQuery $query) {
$filter = new ExactMatchFilter($this->getFullName(), $this->getValue(), $this->getModifiers());
return $filter->apply($query);
}
protected function applyOne(DataQuery $query) {
/* NO OP */
}
public function exclude(DataQuery $query) {
$filter = new ExactMatchFilter($this->getFullName(), $this->getValue(), $this->getModifiers());
return $filter->exclude($query);
}
protected function excludeOne(DataQuery $query) {
/* NO OP */
}
} }

View File

@ -11,24 +11,68 @@
* @subpackage search * @subpackage search
*/ */
class PartialMatchFilter extends SearchFilter { class PartialMatchFilter extends SearchFilter {
protected function comparison($exclude = false) {
$modifiers = $this->getModifiers();
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
throw new InvalidArgumentException(
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
}
if(DB::getConn() instanceof PostgreSQLDatabase) {
if(in_array('case', $modifiers)) {
$comparison = 'LIKE';
} else {
$comparison = 'ILIKE';
}
} elseif(in_array('case', $modifiers)) {
$comparison = 'LIKE BINARY';
} else {
$comparison = 'LIKE';
}
if($exclude) {
$comparison = 'NOT ' . $comparison;
}
return $comparison;
}
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$comparison = $this->comparison(false);
$where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue()));
return $query->where($where);
}
protected function applyMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
$where = array(); $where = array();
$comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE'; $comparison = $this->comparison(false);
if(is_array($this->getValue())) { foreach($this->getValue() as $value) {
foreach($this->getValue() as $value) { $where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value));
$where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value));
}
} else {
$where[] = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue()));
} }
return $query->where(implode(' OR ', $where)); return $query->where(implode(' OR ', $where));
} }
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$comparison = $this->comparison(true);
$where = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue()));
return $query->where($where);
}
protected function excludeMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$where = array();
$comparison = $this->comparison(true);
foreach($this->getValue() as $value) {
$where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value));
}
return $query->where(implode(' AND ', $where));
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -30,6 +30,11 @@ abstract class SearchFilter extends Object {
*/ */
protected $value; protected $value;
/**
* @var array
*/
protected $modifiers;
/** /**
* @var string Name of a has-one, has-many or many-many relation (not the classname). * @var string Name of a has-one, has-many or many-many relation (not the classname).
* Set in the constructor as part of the name in dot-notation, and used in * Set in the constructor as part of the name in dot-notation, and used in
@ -43,12 +48,14 @@ abstract class SearchFilter extends Object {
* the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and * the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and
* search the "Name" column when applying this filter to a SiteTree class). * search the "Name" column when applying this filter to a SiteTree class).
* @param mixed $value * @param mixed $value
* @param array $modifiers
*/ */
public function __construct($fullName, $value = false) { public function __construct($fullName, $value = false, array $modifiers = array()) {
$this->fullName = $fullName; $this->fullName = $fullName;
// sets $this->name and $this->relation // sets $this->name and $this->relation
$this->addRelation($fullName); $this->addRelation($fullName);
$this->value = $value; $this->value = $value;
$this->modifiers = array_map('strtolower', $modifiers);
} }
/** /**
@ -95,6 +102,24 @@ abstract class SearchFilter extends Object {
public function getValue() { public function getValue() {
return $this->value; return $this->value;
} }
/**
* Set the current modifiers to apply to the filter
*
* @param array $modifiers
*/
public function setModifiers(array $modifiers) {
$this->modifiers = array_map('strtolower', $modifiers);
}
/**
* Accessor for the current modifiers to apply to the filter.
*
* @return array
*/
public function getModifiers() {
return $this->modifiers;
}
/** /**
* The original name of the field. * The original name of the field.
@ -173,10 +198,74 @@ abstract class SearchFilter extends Object {
/** /**
* Apply filter criteria to a SQL query. * Apply filter criteria to a SQL query.
* *
* @param SQLQuery $query * @param DataQuery $query
* @return SQLQuery * @return DataQuery
*/ */
abstract public function apply(DataQuery $query); public function apply(DataQuery $query) {
if(($key = array_search('not', $this->modifiers)) !== false) {
unset($this->modifiers[$key]);
return $this->exclude($query);
}
if(is_array($this->value)) {
return $this->applyMany($query);
} else {
return $this->applyOne($query);
}
}
/**
* Apply filter criteria to a SQL query with a single value.
*
* @param DataQuery $query
* @return DataQuery
*/
abstract protected function applyOne(DataQuery $query);
/**
* Apply filter criteria to a SQL query with an array of values.
*
* @param DataQuery $query
* @return DataQuery
*/
protected function applyMany(DataQuery $query) {
throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items.");
}
/**
* Exclude filter criteria from a SQL query.
*
* @param DataQuery $query
* @return DataQuery
*/
public function exclude(DataQuery $query) {
if(($key = array_search('not', $this->modifiers)) !== false) {
unset($this->modifiers[$key]);
return $this->apply($query);
}
if(is_array($this->value)) {
return $this->excludeMany($query);
} else {
return $this->excludeOne($query);
}
}
/**
* Exclude filter criteria from a SQL query with a single value.
*
* @param DataQuery $query
* @return DataQuery
*/
abstract protected function excludeOne(DataQuery $query);
/**
* Exclude filter criteria from a SQL query with an array of values.
*
* @param DataQuery $query
* @return DataQuery
*/
protected function excludeMany(DataQuery $query) {
throw new InvalidArgumentException(get_class($this) . "can't be used to filter by a list of items.");
}
/** /**
* Determines if a field has a value, * Determines if a field has a value,

View File

@ -17,23 +17,102 @@
* @subpackage search * @subpackage search
*/ */
class StartsWithFilter extends SearchFilter { class StartsWithFilter extends SearchFilter {
protected function comparison($exclude = false) {
$modifiers = $this->getModifiers();
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
throw new InvalidArgumentException(
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
}
if(DB::getConn() instanceof PostgreSQLDatabase) {
if(in_array('case', $modifiers)) {
$comparison = 'LIKE';
} else {
$comparison = 'ILIKE';
}
} elseif(in_array('case', $modifiers)) {
$comparison = 'LIKE BINARY';
} else {
$comparison = 'LIKE';
}
if($exclude) {
$comparison = 'NOT ' . $comparison;
}
return $comparison;
}
/** /**
* Applies a substring match on a field value. * Applies a match on the starting characters of a field value.
* *
* @return unknown * @return DataQuery
*/ */
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
return $query->where(sprintf( return $query->where(sprintf(
"%s %s '%s%%'", "%s %s '%s%%'",
$this->getDbName(), $this->getDbName(),
(DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE', $this->comparison(false),
Convert::raw2sql($this->getValue()) Convert::raw2sql($this->getValue())
)); ));
} }
/**
* Applies a match on the starting characters of a field value.
* Matches against one of the many values.
*
* @return DataQuery
*/
protected function applyMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$connectives = array();
foreach($this->getValue() as $value) {
$connectives[] = sprintf(
"%s %s '%s%%'",
$this->getDbName(),
$this->comparison(false),
Convert::raw2sql($value)
);
}
$whereClause = implode(' OR ', $connectives);
return $query->where($whereClause);
}
/**
* Excludes a match on the starting characters of a field value.
*
* @return DataQuery
*/
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s %s '%s%%'",
$this->getDbName(),
$this->comparison(true),
Convert::raw2sql($this->getValue())
));
}
/**
* Excludes a match on the starting characters of a field value.
* Excludes a field if it matches any of the values.
*
* @return DataQuery
*/
protected function excludeMany(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$connectives = array();
foreach($this->getValue() as $value) {
$connectives[] = sprintf(
"%s %s '%s%%'",
$this->getDbName(),
$this->comparison(true),
Convert::raw2sql($value)
);
}
$whereClause = implode(' AND ', $connectives);
return $query->where($whereClause);
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -6,29 +6,47 @@
/** /**
* Checks if a value starts with one of the items of in a given set. * Checks if a value starts with one of the items of in a given set.
* SQL syntax used: Column IN ('val1','val2') * @deprecated 3.1 Use StartsWithFilter instead
* *
* @todo Add negation (NOT IN)6 * @todo Add negation (NOT IN)6
* @package framework * @package framework
* @subpackage search * @subpackage search
*/ */
class StartsWithMultiFilter extends SearchFilter { class StartsWithMultiFilter extends SearchFilter {
function __construct($fullName, $value = false, array $modifiers = array()) {
Deprecation::notice('3.1', 'Use StartsWithFilter instead.');
parent::__construct($fullName, $value, $modifiers);
}
public function apply(DataQuery $query) { public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); if (!is_array($this->getValue())) {
$values = explode(',', $this->getValue()); $values = explode(',',$this->getValue());
} else {
foreach($values as $value) { $values = $this->getValue();
$matches[] = sprintf("%s LIKE '%s%%'",
$this->getDbName(),
Convert::raw2sql(str_replace("'", '', $value))
);
} }
$filter = new StartsWithFilter($this->getFullName(), $values, $this->getModifiers());
return $query->where(implode(" OR ", $matches)); return $filter->apply($query);
}
protected function applyOne(DataQuery $query) {
/* NO OP */
}
public function exclude(DataQuery $query) {
if (!is_array($this->getValue())) {
$values = explode(',',$this->getValue());
} else {
$values = $this->getValue();
}
$filter = new StartsWithFilter($this->getFullName(), $values, $this->getModifiers());
return $filter->exclude($query);
}
protected function excludeOne(DataQuery $query) {
/* NO OP */
} }
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -12,22 +12,33 @@
* @subpackage search * @subpackage search
*/ */
class SubstringFilter extends PartialMatchFilter { class SubstringFilter extends PartialMatchFilter {
public function __construct($fullName, $value = false) { public function __construct($fullName, $value = false, array $modifiers = array()) {
Deprecation::notice('3.0', 'PartialMatchFilter instead.'); Deprecation::notice('3.0', 'PartialMatchFilter instead.');
SearchFilter::__construct($fullName, $value); parent::__construct($fullName, $value, $modifiers);
} }
public function apply(DataQuery $query) { public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $values = $this->getValue();
return $query->where(sprintf( $filter = new PartialMatchFilter($this->getFullName(), $values, $this->getModifiers());
"LOCATE('%s', %s) != 0", return $filter->apply($query);
Convert::raw2sql($this->getValue()),
$this->getDbName()
));
} }
protected function applyOne(DataQuery $query) {
/* NO OP */
}
public function exclude(DataQuery $query) {
$values = $this->getValue();
$filter = new PartialMatchFilter($this->getFullName(), $values, $this->getModifiers());
return $filter->exclude($query);
}
protected function excludeOne(DataQuery $query) {
/* NO OP */
}
public function isEmpty() { public function isEmpty() {
return $this->getValue() == null || $this->getValue() == ''; return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
} }
} }

View File

@ -25,7 +25,7 @@ class WithinRangeFilter extends SearchFilter {
$this->max = $max; $this->max = $max;
} }
public function apply(DataQuery $query) { protected function applyOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation); $this->model = $query->applyRelation($this->relation);
return $query->where(sprintf( return $query->where(sprintf(
"%s >= '%s' AND %s <= '%s'", "%s >= '%s' AND %s <= '%s'",
@ -35,6 +35,15 @@ class WithinRangeFilter extends SearchFilter {
Convert::raw2sql($this->max) Convert::raw2sql($this->max)
)); ));
} }
}
protected function excludeOne(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where(sprintf(
"%s < '%s' OR %s > '%s'",
$this->getDbName(),
Convert::raw2sql($this->min),
$this->getDbName(),
Convert::raw2sql($this->max)
));
}
}

View File

@ -442,10 +442,21 @@ class DataListTest extends SapphireTest {
$list = $list->filter(array( $list = $list->filter(array(
'Name'=>array('Bob','Phil'), 'Name'=>array('Bob','Phil'),
'TeamID'=>array($this->idFromFixture('DataObjectTest_Team', 'team1')))); 'TeamID'=>array($this->idFromFixture('DataObjectTest_Team', 'team1'))));
$this->assertEquals(1, $list->count(), 'There should be one comments'); $this->assertEquals(1, $list->count(), 'There should be one comment');
$this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob'); $this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob');
} }
public function testFilterWithModifiers() {
$list = DataObjectTest_TeamComment::get();
$nocaseList = $list->filter('Name:nocase', 'bob');
$this->assertEquals(1, $nocaseList->count(), 'There should be one comment');
$caseList = $list->filter('Name:case', 'bob');
$this->assertEquals(0, $caseList->count(), 'There should be no comments');
$gtList = $list->filter('TeamID:GreaterThan:not',
$this->idFromFixture('DataObjectTest_Team', 'team1'));
$this->assertEquals(2, $gtList->count());
}
public function testFilterAndExcludeById() { public function testFilterAndExcludeById() {
$id = $this->idFromFixture('DataObjectTest_SubTeam', 'subteam1'); $id = $this->idFromFixture('DataObjectTest_SubTeam', 'subteam1');
$list = DataObjectTest_SubTeam::get()->filter('ID', $id); $list = DataObjectTest_SubTeam::get()->filter('ID', $id);
@ -501,7 +512,7 @@ class DataListTest extends SapphireTest {
*/ */
public function testMultipleExclude() { public function testMultipleExclude() {
$list = DataObjectTest_TeamComment::get(); $list = DataObjectTest_TeamComment::get();
$list->exclude(array('Name'=>'Bob', 'Comment'=>'This is a team comment by Bob')); $list = $list->exclude(array('Name'=>'Bob', 'Comment'=>'This is a team comment by Bob'));
$this->assertEquals(2, $list->count()); $this->assertEquals(2, $list->count());
} }
@ -514,9 +525,17 @@ class DataListTest extends SapphireTest {
$list = $list->exclude('Name', 'Bob'); $list = $list->exclude('Name', 'Bob');
$this->assertContains( $this->assertContains(
'WHERE ("Comment" = \'Phil is a unique guy, and comments on team2\') AND ("Name" != \'Bob\')', 'WHERE ("DataObjectTest_TeamComment"."Comment" = '
. '\'Phil is a unique guy, and comments on team2\') '
. 'AND (("DataObjectTest_TeamComment"."Name" != \'Bob\'))',
$list->sql()); $list->sql());
} }
public function testExcludeWithSearchFilter() {
$list = DataObjectTest_TeamComment::get();
$list = $list->exclude('Name:LessThan', 'Bob');
$this->assertContains('WHERE (("DataObjectTest_TeamComment"."Name" >= \'Bob\'))', $list->sql());
}
/** /**
* $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 * $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43