diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index c442678e9..bb437734a 100755 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -161,12 +161,30 @@ Then there is the most complex task when you want to find Sam and Sig that has e 'FirstName' => array('Sam', 'Sig'), 'Age' => array(17, 74) )); + // SQL: WHERE ("FirstName" IN ('Sam', 'Sig) AND "Age" IN ('17', '74)) -This would be equivalent to a SQL query of +In case you want to match multiple criteria non-exclusively (with an "OR" disjunctive), +use the `filterAny()` method instead: - ::: - ... WHERE ("FirstName" IN ('Sam', 'Sig) AND "Age" IN ('17', '74)); + :::php + $members = Member::get()->filterAny(array( + 'FirstName' => 'Sam', + 'Age' => 17, + )); + // SQL: WHERE ("FirstName" = 'Sam' OR "Age" = '17') +You can also combine both conjunctive ("AND") and disjunctive ("OR") statements. + + :::php + $members = Member::get() + ->filter(array( + 'LastName' => 'Minnée' + )) + ->filterAny(array( + 'FirstName' => 'Sam', + 'Age' => 17, + )); + // SQL: WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17')) ### Exclude diff --git a/model/DataList.php b/model/DataList.php index dd0a4387f..d63f7fa01 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -385,6 +385,68 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab return $this; } + /** + * Return a copy of this list which does not contain items matching any of these charactaristics. + * + * @example // filter bob from list + * $list = $list->filterAny('Name', 'bob'); + * // SQL: WHERE "Name" = 'bob' + * @example // filter aziz and bob from list + * $list = $list->filterAny('Name', array('aziz', 'bob'); + * // SQL: WHERE ("Name" IN ('aziz','bob')) + * @example // filter by bob or anybody aged 21 + * $list = $list->filterAny(array('Name'=>'bob, 'Age'=>21)); + * // SQL: WHERE ("Name" = 'bob' OR "Age" = '21') + * @example // filter by bob or anybody aged 21 or 43 + * $list = $list->filterAny(array('Name'=>'bob, 'Age'=>array(21, 43))); + * // SQL: WHERE ("Name" = 'bob' OR ("Age" IN ('21', '43')) + * @example // bob age 21 or 43, phil age 21 or 43 would be excluded + * $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); + * // SQL: WHERE (("Name" IN ('bob', 'phil')) OR ("Age" IN ('21', '43')) + * + * @todo extract the sql from this method into a SQLGenerator class + * + * @param string|array See {@link filter()} + * @return DataList + */ + public function filterAny() { + $numberFuncArgs = count(func_get_args()); + $whereArguments = array(); + + if($numberFuncArgs == 1 && is_array(func_get_arg(0))) { + $whereArguments = func_get_arg(0); + } elseif($numberFuncArgs == 2) { + $whereArguments[func_get_arg(0)] = func_get_arg(1); + } else { + throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()'); + } + + return $this->alterDataQuery(function($query, $list) use ($whereArguments) { + $subquery = $query->disjunctiveGroup(); + + foreach($whereArguments as $field => $value) { + $fieldArgs = explode(':', $field); + $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->apply($subquery); + } + }); + } + /** * Filter this DataList by a callback function. * The function will be passed each record of the DataList in turn, and must return true for the record to be diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 59b36850d..2d5adb35a 100644 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -457,6 +457,91 @@ class DataListTest extends SapphireTest { $this->assertEquals(2, $gtList->count()); } + public function testFilterAny() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->filterAny('Name', 'Bob'); + $this->assertEquals(1, $list->count()); + } + + public function testFilterAnyMultipleArray() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->filterAny(array('Name'=>'Bob', 'Comment'=>'This is a team comment by Bob')); + $this->assertEquals(1, $list->count()); + $this->assertEquals('Bob', $list->first()->Name, 'Only comment should be from Bob'); + } + + public function testFilterAnyOnFilter() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->filter(array( + 'TeamID'=>$this->idFromFixture('DataObjectTest_Team', 'team1') + )); + $list = $list->filterAny(array( + 'Name'=>array('Phil', 'Joe'), + 'Comment'=>'This is a team comment by Bob' + )); + $list = $list->sort('Name'); + $this->assertEquals(2, $list->count()); + $this->assertEquals( + 'Bob', + $list->offsetGet(0)->Name, + 'Results should include comments from Bob, matched by comment and team' + ); + $this->assertEquals( + 'Joe', + $list->offsetGet(1)->Name, + 'Results should include comments by Joe, matched by name and team (not by comment)' + ); + + $list = DataObjectTest_TeamComment::get(); + $list = $list->filter(array( + 'TeamID'=>$this->idFromFixture('DataObjectTest_Team', 'team1') + )); + $list = $list->filterAny(array( + 'Name'=>array('Phil', 'Joe'), + 'Comment'=>'This is a team comment by Bob' + )); + $list = $list->sort('Name'); + $list = $list->filter(array('Name' => 'Bob')); + $this->assertEquals(1, $list->count()); + $this->assertEquals( + 'Bob', + $list->offsetGet(0)->Name, + 'Results should include comments from Bob, matched by name and team' + ); + } + + public function testFilterAnyMultipleWithArrayFilter() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->filterAny(array('Name'=>array('Bob','Phil'))); + $this->assertEquals(2, $list->count(), 'There should be two comments'); + $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); + $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); + } + + public function testFilterAnyArrayInArray() { + $list = DataObjectTest_TeamComment::get(); + $list = $list->filterAny(array( + 'Name'=>array('Bob','Phil'), + 'TeamID'=>array($this->idFromFixture('DataObjectTest_Team', 'team1')))) + ->sort('Name'); + $this->assertEquals(3, $list->count()); + $this->assertEquals( + 'Bob', + $list->offsetGet(0)->Name, + 'Results should include comments from Bob, matched by name and team' + ); + $this->assertEquals( + 'Joe', + $list->offsetGet(1)->Name, + 'Results should include comments by Joe, matched by team (not by name)' + ); + $this->assertEquals( + 'Phil', + $list->offsetGet(2)->Name, + 'Results should include comments from Phil, matched by name (even if he\'s not in Team1)' + ); + } + public function testFilterAndExcludeById() { $id = $this->idFromFixture('DataObjectTest_SubTeam', 'subteam1'); $list = DataObjectTest_SubTeam::get()->filter('ID', $id);