-
Notifications
You must be signed in to change notification settings - Fork 823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW Enable ArrayList and EagerLoadedList to use search filters #10925
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,12 @@ | |
use ArrayIterator; | ||
use InvalidArgumentException; | ||
use LogicException; | ||
use SilverStripe\Core\ClassInfo; | ||
use SilverStripe\Core\Injector\Injector; | ||
use SilverStripe\Dev\Debug; | ||
use SilverStripe\Dev\Deprecation; | ||
use SilverStripe\ORM\Filters\SearchFilter; | ||
use SilverStripe\ORM\Filters\SearchFilterable; | ||
use SilverStripe\View\ArrayData; | ||
use SilverStripe\View\ViewableData; | ||
use Traversable; | ||
|
@@ -26,6 +31,15 @@ | |
*/ | ||
class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, Limitable | ||
{ | ||
use SearchFilterable; | ||
|
||
/** | ||
* Whether filter and exclude calls should be case sensitive by default or not. | ||
* This configuration property is here for backwards compatability. | ||
* | ||
* @deprecated 5.1.0 use SearchFilter.default_case_sensitive instead | ||
*/ | ||
private static bool $default_case_sensitive = true; | ||
|
||
/** | ||
* Holds the items in the list | ||
|
@@ -368,23 +382,6 @@ public function map($keyfield = 'ID', $titlefield = 'Title') | |
return new Map($list, $keyfield, $titlefield); | ||
} | ||
|
||
/** | ||
* Find the first item of this list where the given key = value | ||
* | ||
* @param string $key | ||
* @param string $value | ||
* @return mixed | ||
*/ | ||
public function find($key, $value) | ||
{ | ||
foreach ($this->items as $item) { | ||
if ($this->extractValue($item, $key) == $value) { | ||
return $item; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Returns an array of a single field value for all items in the list. | ||
* | ||
|
@@ -594,6 +591,18 @@ public function canFilterBy($by) | |
return property_exists($firstRecord, $by ?? ''); | ||
} | ||
|
||
/** | ||
* Find the first item of this list where the given key = value | ||
* | ||
* @param string $key | ||
* @param string $value | ||
* @return mixed | ||
*/ | ||
public function find($key, $value) | ||
{ | ||
return $this->filter($key, $value)->first(); | ||
} | ||
|
||
/** | ||
* Filter the list to include items with these characteristics | ||
* | ||
|
@@ -605,31 +614,15 @@ public function canFilterBy($by) | |
* @example $list->filter(array('Name'=>'bob, 'Age'=>array(21, 43))); // bob with the Age 21 or 43 | ||
* @example $list->filter(array('Name'=>array('aziz','bob'), 'Age'=>array(21, 43))); | ||
* // aziz with the age 21 or 43 and bob with the Age 21 or 43 | ||
* | ||
* Also supports SearchFilter syntax | ||
* @example // include anyone with "sam" anywhere in their name | ||
* $list = $list->filter('Name:PartialMatch', 'sam'); | ||
*/ | ||
public function filter() | ||
{ | ||
|
||
$keepUs = call_user_func_array([$this, 'normaliseFilterArgs'], func_get_args()); | ||
|
||
$itemsToKeep = []; | ||
foreach ($this->items as $item) { | ||
$keepItem = true; | ||
foreach ($keepUs as $column => $value) { | ||
if ((is_array($value) && !in_array($this->extractValue($item, $column), $value ?? [])) | ||
|| (!is_array($value) && $this->extractValue($item, $column) != $value) | ||
) { | ||
$keepItem = false; | ||
break; | ||
} | ||
} | ||
if ($keepItem) { | ||
$itemsToKeep[] = $item; | ||
} | ||
} | ||
|
||
$list = clone $this; | ||
$list->items = $itemsToKeep; | ||
return $list; | ||
$filters = $this->normaliseFilterArgs(...func_get_args()); | ||
return $this->filterOrExclude($filters); | ||
} | ||
|
||
/** | ||
|
@@ -646,28 +639,118 @@ public function filter() | |
* @example // all bobs, phils or anyone aged 21 or 43 in the list | ||
* $list = $list->filterAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); | ||
* | ||
* Also supports SearchFilter syntax | ||
* @example // include anyone with "sam" anywhere in their name | ||
* $list = $list->filterAny('Name:PartialMatch', 'sam'); | ||
* | ||
* @param string|array See {@link filter()} | ||
* @return static | ||
*/ | ||
public function filterAny() | ||
{ | ||
$keepUs = $this->normaliseFilterArgs(...func_get_args()); | ||
$filters = $this->normaliseFilterArgs(...func_get_args()); | ||
return $this->filterOrExclude($filters, true, true); | ||
} | ||
|
||
/** | ||
* Exclude the list to not contain items with these characteristics | ||
* | ||
* @return ArrayList | ||
* @see SS_List::exclude() | ||
* @example $list->exclude('Name', 'bob'); // exclude bob from list | ||
* @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list | ||
* @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 | ||
* @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 | ||
* @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); | ||
* // bob age 21 or 43, phil age 21 or 43 would be excluded | ||
* | ||
* Also supports SearchFilter syntax | ||
* @example // everyone except anyone with "sam" anywhere in their name | ||
* $list = $list->exclude('Name:PartialMatch', 'sam'); | ||
*/ | ||
public function exclude() | ||
{ | ||
$filters = $this->normaliseFilterArgs(...func_get_args()); | ||
return $this->filterOrExclude($filters, false); | ||
} | ||
|
||
/** | ||
* Return a copy of the list excluding any items that have any of these characteristics | ||
* | ||
* @example // everyone except bob in the list | ||
* $list = $list->excludeAny('Name', 'bob'); | ||
* @example // everyone except azis or bob in the list | ||
* $list = $list->excludeAny('Name', array('aziz', 'bob'); | ||
* @example // everyone except bob or anyone aged 21 in the list | ||
* $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>21)); | ||
* @example // everyone except bob or anyone aged 21 or 43 in the list | ||
* $list = $list->excludeAny(array('Name'=>'bob, 'Age'=>array(21, 43))); | ||
* @example // everyone except all bobs, phils or anyone aged 21 or 43 in the list | ||
* $list = $list->excludeAny(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); | ||
* | ||
* Also supports SearchFilter syntax | ||
* @example // everyone except anyone with "sam" anywhere in their name | ||
* $list = $list->excludeAny('Name:PartialMatch', 'sam'); | ||
* | ||
* @param string|array See {@link filter()} | ||
*/ | ||
public function excludeAny(): static | ||
{ | ||
$filters = $this->normaliseFilterArgs(...func_get_args()); | ||
return $this->filterOrExclude($filters, false, true); | ||
} | ||
|
||
/** | ||
* Apply the appropriate filtering or excluding | ||
*/ | ||
protected function filterOrExclude(array $filters, bool $inclusive = true, bool $any = false): static | ||
{ | ||
$itemsToKeep = []; | ||
$searchFilters = []; | ||
|
||
foreach ($filters as $filterKey => $filterValue) { | ||
// Convert null to an empty string for backwards compatability, since nulls are treated specially | ||
// in the ExactMatchFilter | ||
Comment on lines
+712
to
+713
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to hae a Deprecation::notice() when it finds a null $filterValue and say that the behaviour will be changed in CMS 6? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so - we're not going to deprecate passing in |
||
$searchFilter = $this->createSearchFilter($filterKey, $filterValue ?? ''); | ||
|
||
// Apply default case sensitivity for backwards compatability | ||
if (!str_contains($filterKey, ':case') && !str_contains($filterKey, ':nocase')) { | ||
$caseSensitive = Deprecation::withNoReplacement(fn() => static::config()->get('default_case_sensitive')); | ||
if ($caseSensitive && in_array('case', $searchFilter->getSupportedModifiers())) { | ||
$searchFilter->setModifiers($searchFilter->getModifiers() + ['case']); | ||
} elseif (!$caseSensitive && in_array('nocase', $searchFilter->getSupportedModifiers())) { | ||
$searchFilter->setModifiers($searchFilter->getModifiers() + ['nocase']); | ||
} | ||
} | ||
Comment on lines
+716
to
+724
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ArrayList was implicitly case sensitive before, because it was just doing a simple |
||
|
||
$searchFilters[$filterKey] = $searchFilter; | ||
} | ||
|
||
foreach ($this->items as $item) { | ||
foreach ($keepUs as $column => $value) { | ||
$extractedValue = $this->extractValue($item, $column); | ||
$matches = is_array($value) ? in_array($extractedValue, $value) : $extractedValue == $value; | ||
if ($matches) { | ||
$itemsToKeep[] = $item; | ||
$matches = []; | ||
foreach ($filters as $filterKey => $filterValue) { | ||
/** @var SearchFilter $searchFilter */ | ||
$searchFilter = $searchFilters[$filterKey]; | ||
$hasMatch = $searchFilter->matches($this->extractValue($item, $searchFilter->getFullName()) ?? ''); | ||
$matches[$hasMatch] = 1; | ||
// If this is excludeAny or filterAny and we have a match, we can stop looking for matches. | ||
if ($any && $hasMatch) { | ||
break; | ||
} | ||
} | ||
|
||
// filterAny or excludeAny allow any true value to be a match; filter or exclude require any false value | ||
// to be a mismatch. | ||
$isMatch = $any ? isset($matches[true]) : !isset($matches[false]); | ||
|
||
// If inclusive (filter) and we have a match, or exclusive (exclude) and there is NO match, keep the item. | ||
if (($inclusive && $isMatch) || (!$inclusive && !$isMatch)) { | ||
$itemsToKeep[] = $item; | ||
} | ||
} | ||
|
||
$list = clone $this; | ||
$list->items = array_unique($itemsToKeep ?? [], SORT_REGULAR); | ||
$list->items = $itemsToKeep; | ||
Comment on lines
-670
to
+753
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We absolutely shouldn't be sorting here. That's what the sort method is for. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah shouldn't .... you could argue it is a behaviour change so existing data on a website could change its presentation after upgrade. However, I'd counter and say we're fixing a bug here. |
||
return $list; | ||
} | ||
|
||
|
@@ -755,48 +838,6 @@ public function filterByCallback($callback) | |
return $output; | ||
} | ||
|
||
/** | ||
* Exclude the list to not contain items with these characteristics | ||
* | ||
* @return ArrayList | ||
* @see SS_List::exclude() | ||
* @example $list->exclude('Name', 'bob'); // exclude bob from list | ||
* @example $list->exclude('Name', array('aziz', 'bob'); // exclude aziz and bob from list | ||
* @example $list->exclude(array('Name'=>'bob, 'Age'=>21)); // exclude bob that has Age 21 | ||
* @example $list->exclude(array('Name'=>'bob, 'Age'=>array(21, 43))); // exclude bob with Age 21 or 43 | ||
* @example $list->exclude(array('Name'=>array('bob','phil'), 'Age'=>array(21, 43))); | ||
* // bob age 21 or 43, phil age 21 or 43 would be excluded | ||
*/ | ||
public function exclude() | ||
{ | ||
$removeUs = $this->normaliseFilterArgs(...func_get_args()); | ||
|
||
$hitsRequiredToRemove = count($removeUs ?? []); | ||
$matches = []; | ||
foreach ($removeUs as $column => $excludeValue) { | ||
foreach ($this->items as $key => $item) { | ||
if (!is_array($excludeValue) && $this->extractValue($item, $column) == $excludeValue) { | ||
$matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1; | ||
} elseif (is_array($excludeValue) && in_array($this->extractValue($item, $column), $excludeValue ?? [])) { | ||
$matches[$key] = isset($matches[$key]) ? $matches[$key] + 1 : 1; | ||
} | ||
} | ||
} | ||
|
||
$keysToRemove = array_keys($matches ?? [], $hitsRequiredToRemove); | ||
|
||
$itemsToKeep = []; | ||
foreach ($this->items as $key => $value) { | ||
if (!in_array($key, $keysToRemove ?? [])) { | ||
$itemsToKeep[] = $value; | ||
} | ||
} | ||
|
||
$list = clone $this; | ||
$list->items = $itemsToKeep; | ||
return $list; | ||
} | ||
|
||
protected function shouldExclude($item, $args) | ||
{ | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved a couple of methods in this class to be next to each other where they have similar functionality. I've only moved methods that are already being changed in this PR anyway.