diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index e7ddf4069..285ddb7b1 100755 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -210,6 +210,25 @@ You can also combine both conjunctive ("AND") and disjunctive ("OR") statements. 'Age' => 17, )); // WHERE ("LastName" = 'Minnée' AND ("FirstName" = 'Sam' OR "Age" = '17')) + +### Filter with PHP / filterByCallback + +It is also possible to filter by a PHP callback, however this will force the +data model to fetch all records and loop them in PHP, thus `filter()` or `filterAny()` +are to be preferred over `filterByCallback()`. +Please note that because `filterByCallback()` has to run in PHP, it will always return +an `ArrayList` (even if called on a `DataList`, this however might change in future). +The first parameter to the callback is the item, the second parameter is the list itself. +The callback will run once for each record, if the callback returns true, this record +will be added to the list of returned items. +The below example will get all Members that have an expired or not encrypted password. + + :::php + $membersWithBadPassword = Member::get()->filterByCallback(function($item, $list) { + if ($item->isPasswordExpired() || $item->PasswordEncryption = 'none') { + return true; + } + }); ### Exclude diff --git a/model/ArrayList.php b/model/ArrayList.php index 80f8d6682..e0d4d1616 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -476,6 +476,27 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta return $firstElement; } + /** + * @see SS_Filterable::filterByCallback() + * + * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; }) + * @param callable $callback + * @return ArrayList + */ + public function filterByCallback($callback) { + if(!is_callable($callback)) { + throw new LogicException(sprintf( + "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given", + gettype($callback) + )); + } + $output = ArrayList::create(); + foreach($this as $item) { + if(call_user_func($callback, $item, $this)) $output->push($item); + } + return $output; + } + /** * Exclude the list to not contain items with these charactaristics * diff --git a/model/DataList.php b/model/DataList.php index 6cb646d61..a397b3a16 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -39,7 +39,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab /** * The DataModel from which this DataList comes. - * + * * @var DataModel */ protected $model; @@ -406,21 +406,24 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab } /** - * 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 - * included. Returns the filtered list. - * * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a * future implementation. + * @see SS_Filterable::filterByCallback() + * + * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; }) + * @param callable $callback + * @return ArrayList (this may change in future implementations) */ public function filterByCallback($callback) { if(!is_callable($callback)) { - throw new LogicException("DataList::filterByCallback() must be passed something callable."); + throw new LogicException(sprintf( + "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given", + gettype($callback) + )); } - - $output = new ArrayList; + $output = ArrayList::create(); foreach($this as $item) { - if($callback($item)) $output->push($item); + if(call_user_func($callback, $item, $this)) $output->push($item); } return $output; } diff --git a/model/Filterable.php b/model/Filterable.php index 72d0d918f..cd3bee3a9 100644 --- a/model/Filterable.php +++ b/model/Filterable.php @@ -30,7 +30,7 @@ interface SS_Filterable { * // aziz with the age 21 or 43 and bob with the Age 21 or 43 */ public function filter(); - + /** * Return a new instance of this list that excludes any items with these charactaristics * @@ -43,5 +43,14 @@ interface SS_Filterable { * // bob age 21 or 43, phil age 21 or 43 would be excluded */ public function exclude(); - + + /** + * Return a new instance of this list that excludes any items with these charactaristics + * Filter this List by a callback function. The function will be passed each record of the List in turn, + * and must return true for the record to be included. Returns the filtered list. + * + * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; }) + * @return SS_Filterable + */ + public function filterByCallback($callback); } diff --git a/model/ListDecorator.php b/model/ListDecorator.php index a1af521eb..3a9e9ffe4 100644 --- a/model/ListDecorator.php +++ b/model/ListDecorator.php @@ -147,6 +147,29 @@ abstract class SS_ListDecorator extends ViewableData implements SS_List, SS_Sort return call_user_func_array(array($this->list, 'filter'), $args); } + /** + * Note that, in the current implementation, the filtered list will be an ArrayList, but this may change in a + * future implementation. + * @see SS_Filterable::filterByCallback() + * + * @example $list = $list->filterByCallback(function($item, $list) { return $item->Age == 9; }) + * @param callable $callback + * @return ArrayList (this may change in future implementations) + */ + public function filterByCallback($callback) { + if(!is_callable($callback)) { + throw new LogicException(sprintf( + "SS_Filterable::filterByCallback() passed callback must be callable, '%s' given", + gettype($callback) + )); + } + $output = ArrayList::create(); + foreach($this->list as $item) { + if(call_user_func($callback, $item, $this->list)) $output->push($item); + } + return $output; + } + public function limit($limit, $offset = 0) { return $this->list->limit($limit, $offset); } diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index 9953c5200..7f0e2477e 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -438,6 +438,32 @@ class ArrayListTest extends SapphireTest { $this->assertEquals(3, $list->count()); $this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Steve and Clair'); } + + /** + * $list = $list->filterByCallback(function($item, $list) { return $item->Age == 21; }) + */ + public function testFilterByCallback() { + $list = new ArrayList(array( + array('Name' => 'Steve', 'ID' => 1, 'Age' => 21), + array('Name' => 'Bob', 'ID' => 2, 'Age' => 18), + array('Name' => 'Clair', 'ID' => 2, 'Age' => 21), + array('Name' => 'Oscar', 'ID' => 2, 'Age' => 52), + array('Name' => 'Mike', 'ID' => 3, 'Age' => 43) + )); + + $list = $list->filterByCallback(function ($item, $list) { + return $item->Age == 21; + }); + + $expected = array( + new ArrayData(array('Name' => 'Steve', 'ID' => 1, 'Age' => 21)), + new ArrayData(array('Name' => 'Clair', 'ID' => 2, 'Age' => 21)), + ); + + $this->assertEquals(2, $list->count()); + $this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Clair'); + $this->assertTrue($list instanceof SS_Filterable, 'The List should be of type SS_Filterable'); + } /** * $list->exclude('Name', 'bob'); // exclude bob from list diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 90f2bac95..d9112dcf6 100644 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -671,6 +671,24 @@ class DataListTest extends SapphireTest { $this->assertEquals(0, $list->exclude('ID', $obj->ID)->count()); } + /** + * $list = $list->filterByCallback(function($item, $list) { return $item->Age == 21; }) + */ + public function testFilterByCallback() { + $team1ID = $this->idFromFixture('DataObjectTest_Team', 'team1'); + $list = DataObjectTest_TeamComment::get(); + $list = $list->filterByCallback(function ($item, $list) use ($team1ID) { + return $item->TeamID == $team1ID; + }); + + $result = $list->column('Name'); + $expected = array_intersect($result, array('Joe', 'Bob')); + + $this->assertEquals(2, $list->count()); + $this->assertEquals($expected, $result, 'List should only contain comments from Team 1 (Joe and Bob)'); + $this->assertTrue($list instanceof SS_Filterable, 'The List should be of type SS_Filterable'); + } + /** * $list->exclude('Name', 'bob'); // exclude bob from list */