mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Enable ArrayList and EagerLoadedList to use search filters (#10925)
This commit is contained in:
parent
c17138b6f5
commit
b4463d9050
@ -5,7 +5,12 @@ namespace SilverStripe\ORM;
|
||||
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 @@ use Traversable;
|
||||
*/
|
||||
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 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
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 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
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 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
* @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 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
* @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
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
return $list;
|
||||
}
|
||||
|
||||
@ -755,48 +838,6 @@ class ArrayList extends ViewableData implements SS_List, Filterable, Sortable, L
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ namespace SilverStripe\ORM;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\ORM\Filters\SearchFilter;
|
||||
use SilverStripe\ORM\Queries\SQLConditionGroup;
|
||||
use SilverStripe\View\ViewableData;
|
||||
use Exception;
|
||||
@ -15,6 +14,7 @@ use SilverStripe\ORM\Connect\Query;
|
||||
use Traversable;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\Filters\SearchFilterable;
|
||||
|
||||
/**
|
||||
* Implements a "lazy loading" DataObjectSet.
|
||||
@ -38,6 +38,8 @@ use SilverStripe\ORM\ArrayList;
|
||||
*/
|
||||
class DataList extends ViewableData implements SS_List, Filterable, Sortable, Limitable
|
||||
{
|
||||
use SearchFilterable;
|
||||
|
||||
/**
|
||||
* Whether to use placeholders for integer IDs on Primary and Foriegn keys during a WHERE IN query
|
||||
* It is significantly faster to not use placeholders
|
||||
@ -665,44 +667,6 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
|
||||
return preg_match('/^[A-Z0-9\._]+$/i', $field ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a filter expression and value construct a {@see SearchFilter} instance
|
||||
*
|
||||
* @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
|
||||
* @param mixed $value Value of the filter
|
||||
* @return SearchFilter
|
||||
*/
|
||||
protected function createSearchFilter($filter, $value)
|
||||
{
|
||||
// Field name is always the first component
|
||||
$fieldArgs = explode(':', $filter ?? '');
|
||||
$fieldName = array_shift($fieldArgs);
|
||||
|
||||
// Inspect type of second argument to determine context
|
||||
$secondArg = array_shift($fieldArgs);
|
||||
$modifiers = $fieldArgs;
|
||||
if (!$secondArg) {
|
||||
// Use default filter if none specified. E.g. `->filter(['Name' => $myname])`
|
||||
$filterServiceName = 'DataListFilter.default';
|
||||
} else {
|
||||
// The presence of a second argument is by default ambiguous; We need to query
|
||||
// Whether this is a valid modifier on the default filter, or a filter itself.
|
||||
/** @var SearchFilter $defaultFilterInstance */
|
||||
$defaultFilterInstance = Injector::inst()->get('DataListFilter.default');
|
||||
if (in_array(strtolower($secondArg ?? ''), $defaultFilterInstance->getSupportedModifiers() ?? [])) {
|
||||
// Treat second (and any subsequent) argument as modifiers, using default filter
|
||||
$filterServiceName = 'DataListFilter.default';
|
||||
array_unshift($modifiers, $secondArg);
|
||||
} else {
|
||||
// Second argument isn't a valid modifier, so assume is filter identifier
|
||||
$filterServiceName = "DataListFilter.{$secondArg}";
|
||||
}
|
||||
}
|
||||
|
||||
// Build instance
|
||||
return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a copy of this list which does not contain any items that match all params
|
||||
*
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\ORM\FieldType\DBField;
|
||||
use BadMethodCallException;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use SilverStripe\ORM\Filters\SearchFilterable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
@ -23,6 +24,8 @@ use Traversable;
|
||||
*/
|
||||
class EagerLoadedList extends ViewableData implements Relation, SS_List, Filterable, Sortable, Limitable
|
||||
{
|
||||
use SearchFilterable;
|
||||
|
||||
/**
|
||||
* List responsible for instantiating the actual DataObject objects from eager-loaded data
|
||||
*/
|
||||
@ -545,7 +548,7 @@ class EagerLoadedList extends ViewableData implements Relation, SS_List, Filtera
|
||||
throw new InvalidArgumentException("Incorrect number of arguments passed to $function");
|
||||
}
|
||||
foreach (array_keys($filter) as $column) {
|
||||
if (!$this->canFilterBy($column)) {
|
||||
if (!$this->canFilterBy(explode(':', $column)[0])) {
|
||||
throw new InvalidArgumentException("Can't filter by column '$column'");
|
||||
}
|
||||
}
|
||||
@ -561,12 +564,23 @@ class EagerLoadedList extends ViewableData implements Relation, SS_List, Filtera
|
||||
private function getMatches($filters, bool $any = false): array
|
||||
{
|
||||
$matches = [];
|
||||
$searchFilters = [];
|
||||
|
||||
foreach ($filters as $filterKey => $filterValue) {
|
||||
$searchFilters[$filterKey] = $this->createSearchFilter($filterKey, $filterValue);
|
||||
}
|
||||
|
||||
foreach ($this->rows as $id => $row) {
|
||||
$doesMatch = true;
|
||||
foreach ($filters as $column => $value) {
|
||||
$extractedValue = $this->extractValue($row, $this->standardiseColumn($column));
|
||||
$strict = $value === null || $extractedValue === null;
|
||||
$doesMatch = $this->doesMatch($column, $value, $extractedValue, $strict);
|
||||
// Throw exception for empty $value arrays to match ExactMatchFilter::manyFilter
|
||||
if (is_array($value) && empty($value)) {
|
||||
throw new InvalidArgumentException("Cannot filter $column against an empty set");
|
||||
}
|
||||
/** @var SearchFilter $searchFilter */
|
||||
$searchFilter = $searchFilters[$column];
|
||||
$extractedValue = $this->extractValue($row, $this->standardiseColumn($searchFilter->getFullName()));
|
||||
$doesMatch = $searchFilter->matches($extractedValue);
|
||||
if (!$any && !$doesMatch) {
|
||||
$doesMatch = false;
|
||||
break;
|
||||
@ -582,23 +596,6 @@ class EagerLoadedList extends ViewableData implements Relation, SS_List, Filtera
|
||||
return $matches;
|
||||
}
|
||||
|
||||
private function doesMatch(string $field, mixed $value1, mixed $value2, bool $strict): bool
|
||||
{
|
||||
if (is_array($value1)) {
|
||||
if (empty($value1)) {
|
||||
// mimics ExactMatchFilter::manyFilter
|
||||
throw new InvalidArgumentException("Cannot filter $field against an empty set");
|
||||
}
|
||||
return in_array($value2, $value1, $strict);
|
||||
}
|
||||
|
||||
if ($strict) {
|
||||
return $value1 === $value2;
|
||||
}
|
||||
|
||||
return $value1 == $value2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a value from an item in the list, where the item is either an
|
||||
* object or array.
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\ORM\Filters;
|
||||
|
||||
use BadMethodCallException;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
|
||||
/**
|
||||
@ -32,6 +33,46 @@ abstract class ComparisonFilter extends SearchFilter
|
||||
*/
|
||||
abstract protected function getInverseOperator();
|
||||
|
||||
public function matches(mixed $objectValue): bool
|
||||
{
|
||||
$negated = in_array('not', $this->getModifiers());
|
||||
|
||||
// can't just cast to array, because that will convert null into an empty array
|
||||
$filterValues = $this->getValue();
|
||||
if (!is_array($filterValues)) {
|
||||
$filterValues = [$filterValues];
|
||||
}
|
||||
|
||||
// This is essentially a in_array($objectValue, $filterValues) check, with some special handling.
|
||||
$hasMatch = false;
|
||||
foreach ($filterValues as $filterValue) {
|
||||
$doesMatch = $this->match($objectValue, $filterValue);
|
||||
|
||||
// Any match is a match
|
||||
if ($doesMatch) {
|
||||
$hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Respect "not" modifier.
|
||||
if ($negated) {
|
||||
$hasMatch = !$hasMatch;
|
||||
}
|
||||
|
||||
return $hasMatch;
|
||||
}
|
||||
|
||||
protected function match(mixed $objectValue, mixed $filterValue): bool
|
||||
{
|
||||
// We can't add an abstract method because that will mean custom subclasses would need to
|
||||
// implement this new method which makes it a breaking change - but we want to enforce the
|
||||
// method signature for any subclasses which do implement this - therefore, throw an
|
||||
// exception by default.
|
||||
$actualClass = get_class($this);
|
||||
throw new BadMethodCallException("matches is not implemented on $actualClass");
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a comparison filter to the query
|
||||
* Handles SQL escaping for both numeric and string values
|
||||
|
@ -13,6 +13,7 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class EndsWithFilter extends PartialMatchFilter
|
||||
{
|
||||
protected static $matchesEndsWith = true;
|
||||
|
||||
protected function getMatchPattern($value)
|
||||
{
|
||||
|
@ -13,12 +13,54 @@ use SilverStripe\ORM\DataList;
|
||||
/**
|
||||
* Selects textual content with an exact match between columnname and keyword.
|
||||
*
|
||||
* @todo case sensitivity switch
|
||||
* @todo documentation
|
||||
*/
|
||||
class ExactMatchFilter extends SearchFilter
|
||||
{
|
||||
|
||||
public function matches(mixed $objectValue): bool
|
||||
{
|
||||
$isCaseSensitive = $this->getCaseSensitive();
|
||||
if ($isCaseSensitive === null) {
|
||||
$isCaseSensitive = $this->getCaseSensitiveByCollation();
|
||||
}
|
||||
$caseSensitive = $isCaseSensitive ? '' : 'i';
|
||||
$negated = in_array('not', $this->getModifiers());
|
||||
|
||||
// Can't just cast to array, because that will convert null into an empty array
|
||||
$filterValues = $this->getValue();
|
||||
if (!is_array($filterValues)) {
|
||||
$filterValues = [$filterValues];
|
||||
}
|
||||
|
||||
// This is essentially a in_array($objectValue, $filterValues) check, with some special handling.
|
||||
$hasMatch = false;
|
||||
foreach ($filterValues as $filterValue) {
|
||||
if (is_string($filterValue) && is_string($objectValue)) {
|
||||
$regexSafeFilterValue = preg_quote($filterValue, '/');
|
||||
$doesMatch = preg_match('/^' . $regexSafeFilterValue . '$/u' . $caseSensitive, $objectValue);
|
||||
} elseif ($filterValue === null || $objectValue === null) {
|
||||
$doesMatch = $filterValue === $objectValue;
|
||||
} else {
|
||||
// case sensitivity is meaningless if one or both values aren't strings,
|
||||
// so fall back to a loose equivalency comparison.
|
||||
$doesMatch = $filterValue == $objectValue;
|
||||
}
|
||||
// Any match is a match
|
||||
if ($doesMatch) {
|
||||
$hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Respect "not" modifier.
|
||||
if ($negated) {
|
||||
$hasMatch = !$hasMatch;
|
||||
}
|
||||
|
||||
return $hasMatch;
|
||||
}
|
||||
|
||||
public function getSupportedModifiers()
|
||||
{
|
||||
return ['not', 'nocase', 'case'];
|
||||
@ -81,7 +123,7 @@ class ExactMatchFilter extends SearchFilter
|
||||
}
|
||||
|
||||
$clause = [$where => $value];
|
||||
|
||||
|
||||
return $this->aggregate ?
|
||||
$this->applyAggregate($query, $clause) :
|
||||
$query->where($clause);
|
||||
|
@ -10,6 +10,10 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class GreaterThanFilter extends ComparisonFilter
|
||||
{
|
||||
protected function match(mixed $objectValue, mixed $filterValue): bool
|
||||
{
|
||||
return $objectValue > $filterValue;
|
||||
}
|
||||
|
||||
protected function getOperator()
|
||||
{
|
||||
|
@ -10,6 +10,10 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class GreaterThanOrEqualFilter extends ComparisonFilter
|
||||
{
|
||||
protected function match(mixed $objectValue, mixed $filterValue): bool
|
||||
{
|
||||
return $objectValue >= $filterValue;
|
||||
}
|
||||
|
||||
protected function getOperator()
|
||||
{
|
||||
|
@ -10,6 +10,10 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class LessThanFilter extends ComparisonFilter
|
||||
{
|
||||
protected function match(mixed $objectValue, mixed $filterValue): bool
|
||||
{
|
||||
return $objectValue < $filterValue;
|
||||
}
|
||||
|
||||
protected function getOperator()
|
||||
{
|
||||
|
@ -10,6 +10,10 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class LessThanOrEqualFilter extends ComparisonFilter
|
||||
{
|
||||
protected function match(mixed $objectValue, mixed $filterValue): bool
|
||||
{
|
||||
return $objectValue <= $filterValue;
|
||||
}
|
||||
|
||||
protected function getOperator()
|
||||
{
|
||||
|
@ -11,6 +11,8 @@ use InvalidArgumentException;
|
||||
*/
|
||||
class PartialMatchFilter extends SearchFilter
|
||||
{
|
||||
protected static $matchesStartsWith = false;
|
||||
protected static $matchesEndsWith = false;
|
||||
|
||||
public function getSupportedModifiers()
|
||||
{
|
||||
@ -28,6 +30,55 @@ class PartialMatchFilter extends SearchFilter
|
||||
return "%$value%";
|
||||
}
|
||||
|
||||
public function matches(mixed $objectValue): bool
|
||||
{
|
||||
$isCaseSensitive = $this->getCaseSensitive();
|
||||
if ($isCaseSensitive === null) {
|
||||
$isCaseSensitive = $this->getCaseSensitiveByCollation();
|
||||
}
|
||||
$caseSensitive = $isCaseSensitive ? '' : 'i';
|
||||
$negated = in_array('not', $this->getModifiers());
|
||||
$objectValueString = (string) $objectValue;
|
||||
|
||||
// can't just cast to array, because that will convert null into an empty array
|
||||
$filterValues = $this->getValue();
|
||||
if (!is_array($filterValues)) {
|
||||
$filterValues = [$filterValues];
|
||||
}
|
||||
|
||||
// This is essentially a in_array($objectValue, $filterValues) check, with some special handling.
|
||||
$hasMatch = false;
|
||||
foreach ($filterValues as $filterValue) {
|
||||
if (is_bool($objectValue)) {
|
||||
if (static::$matchesStartsWith || static::$matchesEndsWith) {
|
||||
// Nothing "starts" or "ends" with a boolean value, so automatically fail those matches.
|
||||
$doesMatch = false;
|
||||
} else {
|
||||
// A partial boolean match should match truthy and falsy values.
|
||||
$doesMatch = $objectValue == $filterValue;
|
||||
}
|
||||
} else {
|
||||
$filterValue = (string) $filterValue;
|
||||
$regexSafeFilterValue = preg_quote($filterValue, '/');
|
||||
$start = static::$matchesStartsWith ? '^' : '';
|
||||
$end = static::$matchesEndsWith ? '$' : '';
|
||||
$doesMatch = preg_match('/' . $start . $regexSafeFilterValue . $end . '/u' . $caseSensitive, $objectValueString);
|
||||
}
|
||||
// Any match is a match
|
||||
if ($doesMatch) {
|
||||
$hasMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Respect "not" modifier.
|
||||
if ($negated) {
|
||||
$hasMatch = !$hasMatch;
|
||||
}
|
||||
|
||||
return $hasMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter criteria to a SQL query.
|
||||
*
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace SilverStripe\ORM\Filters;
|
||||
|
||||
use BadMethodCallException;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
|
||||
/**
|
||||
@ -26,7 +29,19 @@ use SilverStripe\ORM\FieldType\DBField;
|
||||
*/
|
||||
abstract class SearchFilter
|
||||
{
|
||||
use Injectable;
|
||||
use Injectable, Configurable;
|
||||
|
||||
/**
|
||||
* Whether the database uses case sensitive collation or not.
|
||||
* @internal
|
||||
*/
|
||||
private static ?bool $caseSensitiveByCollation = null;
|
||||
|
||||
/**
|
||||
* Whether search filters should be case sensitive or not by default.
|
||||
* If null, the database collation setting is used.
|
||||
*/
|
||||
private static ?bool $default_case_sensitive = null;
|
||||
|
||||
/**
|
||||
* Classname of the inspected {@link DataObject}.
|
||||
@ -345,6 +360,19 @@ abstract class SearchFilter
|
||||
->groupby("\"{$baseTable}\".\"ID\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this filter matches against a value.
|
||||
*/
|
||||
public function matches(mixed $objectValue): bool
|
||||
{
|
||||
// We can't add an abstract method because that will mean custom subclasses would need to
|
||||
// implement this new method which makes it a breaking change - but we want to enforce the
|
||||
// method signature for any subclasses which do implement this - therefore, throw an
|
||||
// exception by default.
|
||||
$actualClass = get_class($this);
|
||||
throw new BadMethodCallException("matches is not implemented on $actualClass");
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter criteria to a SQL query.
|
||||
*
|
||||
@ -437,7 +465,7 @@ abstract class SearchFilter
|
||||
/**
|
||||
* Determines case sensitivity based on {@link getModifiers()}.
|
||||
*
|
||||
* @return Mixed TRUE or FALSE to enforce sensitivity, NULL to use field collation.
|
||||
* @return ?bool TRUE or FALSE to enforce sensitivity, NULL to use field collation.
|
||||
*/
|
||||
protected function getCaseSensitive()
|
||||
{
|
||||
@ -447,7 +475,27 @@ abstract class SearchFilter
|
||||
} elseif (in_array('nocase', $modifiers ?? [])) {
|
||||
return false;
|
||||
} else {
|
||||
return null;
|
||||
$sensitive = self::config()->get('default_case_sensitive');
|
||||
if ($sensitive !== null) {
|
||||
return $sensitive;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out whether the database is set to use case sensitive comparisons or not by default.
|
||||
* Used for static comparisons in the matches() method.
|
||||
*/
|
||||
protected function getCaseSensitiveByCollation(): ?bool
|
||||
{
|
||||
if (!self::$caseSensitiveByCollation) {
|
||||
if (!DB::is_active()) {
|
||||
return null;
|
||||
}
|
||||
self::$caseSensitiveByCollation = DB::query("SELECT 'CASE' = 'case'")->record() === 0;
|
||||
}
|
||||
|
||||
return self::$caseSensitiveByCollation;
|
||||
}
|
||||
}
|
||||
|
47
src/ORM/Filters/SearchFilterable.php
Normal file
47
src/ORM/Filters/SearchFilterable.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Filters;
|
||||
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
|
||||
trait SearchFilterable
|
||||
{
|
||||
/**
|
||||
* Given a filter expression and value construct a {@see SearchFilter} instance
|
||||
*
|
||||
* @param string $filter E.g. `Name:ExactMatch:not`, `Name:ExactMatch`, `Name:not`, `Name`
|
||||
* @param mixed $value Value of the filter
|
||||
* @return SearchFilter
|
||||
*/
|
||||
protected function createSearchFilter($filter, $value)
|
||||
{
|
||||
// Field name is always the first component
|
||||
$fieldArgs = explode(':', $filter);
|
||||
$fieldName = array_shift($fieldArgs);
|
||||
$default = 'DataListFilter.default';
|
||||
|
||||
// Inspect type of second argument to determine context
|
||||
$secondArg = array_shift($fieldArgs);
|
||||
$modifiers = $fieldArgs;
|
||||
if (!$secondArg) {
|
||||
// Use default SearchFilter if none specified. E.g. `->filter(['Name' => $myname])`
|
||||
$filterServiceName = $default;
|
||||
} else {
|
||||
// The presence of a second argument is by default ambiguous; We need to query
|
||||
// Whether this is a valid modifier on the default filter, or a filter itself.
|
||||
/** @var SearchFilter $defaultFilterInstance */
|
||||
$defaultFilterInstance = Injector::inst()->get($default);
|
||||
if (in_array(strtolower($secondArg), $defaultFilterInstance->getSupportedModifiers() ?? [])) {
|
||||
// Treat second (and any subsequent) argument as modifiers, using default filter
|
||||
$filterServiceName = $default;
|
||||
array_unshift($modifiers, $secondArg);
|
||||
} else {
|
||||
// Second argument isn't a valid modifier, so assume is filter identifier
|
||||
$filterServiceName = "DataListFilter.{$secondArg}";
|
||||
}
|
||||
}
|
||||
|
||||
// Build instance
|
||||
return Injector::inst()->create($filterServiceName, $fieldName, $value, $modifiers);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ namespace SilverStripe\ORM\Filters;
|
||||
*/
|
||||
class StartsWithFilter extends PartialMatchFilter
|
||||
{
|
||||
protected static $matchesStartsWith = true;
|
||||
|
||||
protected function getMatchPattern($value)
|
||||
{
|
||||
|
@ -343,6 +343,59 @@ class ArrayListTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function provideFindWithSearchfilters()
|
||||
{
|
||||
$objects = $this->getFilterWithSearchfiltersObjects();
|
||||
return [
|
||||
// test a couple of search filters
|
||||
// don't need to be as explicit as the filter tests, just check the syntax works
|
||||
'exact match not case sensitive' => [
|
||||
'args' => ['NoCase:nocase', 'case sensitive'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects[0],
|
||||
],
|
||||
'startswith match' => [
|
||||
'args' => ['StartsWithTest:StartsWith', 'test'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects[3],
|
||||
],
|
||||
'startswith match no case' => [
|
||||
'args' => ['StartsWithTest:StartsWith:nocase', 'test'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects[0],
|
||||
],
|
||||
'startswith match negated' => [
|
||||
'args' => ['StartsWithTest:StartsWith:not', 'Test'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects[1],
|
||||
],
|
||||
'lessthan match' => [
|
||||
'args' => ['GreaterThan100:LessThan', '100'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects[2],
|
||||
],
|
||||
'nomatch greaterthan' => [
|
||||
'args' => ['LessThan100:GreaterThan', 1000],
|
||||
'objects' => $objects,
|
||||
'expected' => null,
|
||||
],
|
||||
'nomatch lessthan' => [
|
||||
'args' => ['LessThan100:LessThan:not', 1000],
|
||||
'objects' => $objects,
|
||||
'expected' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideFindWithSearchfilters
|
||||
*/
|
||||
public function testFindWithSearchfilters(array $args, array $objects, object|array|null $expected)
|
||||
{
|
||||
$list = new ArrayList($objects);
|
||||
$this->assertEquals($expected, $list->find(...$args));
|
||||
}
|
||||
|
||||
public function testFind()
|
||||
{
|
||||
$list = new ArrayList(
|
||||
@ -917,73 +970,354 @@ class ArrayListTest extends SapphireTest
|
||||
$this->assertEquals($expected, $list->toArray(), 'List should only contain Steve and Steve and Clair');
|
||||
}
|
||||
|
||||
public function testFilterAny()
|
||||
private function getFilterWithSearchfiltersObjects()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'ID' => 1,
|
||||
'Name' => 'Steve',
|
||||
'Age' => 21,
|
||||
'Title' => 'First Object',
|
||||
'NoCase' => 'CaSe SeNsItIvE',
|
||||
'CaseSensitive' => 'Case Sensitive',
|
||||
'StartsWithTest' => 'Test Value',
|
||||
'GreaterThan100' => 300,
|
||||
'LessThan100' => 50,
|
||||
'SomeField' => 'Some Value',
|
||||
],
|
||||
[
|
||||
'ID' => 2,
|
||||
'Name' => 'Steve',
|
||||
'Age' => 18,
|
||||
'Title' => 'Second Object',
|
||||
'NoCase' => 'case sensitive',
|
||||
'CaseSensitive' => 'case sensitive',
|
||||
'StartsWithTest' => 'Not Starts With Test',
|
||||
'GreaterThan100' => 101,
|
||||
'LessThan100' => 99,
|
||||
'SomeField' => 'Another Value',
|
||||
],
|
||||
[
|
||||
'ID' => 3,
|
||||
'Name' => 'Steve',
|
||||
'Age' => 43,
|
||||
'Title' => 'Third Object',
|
||||
'NoCase' => null,
|
||||
'CaseSensitive' => '',
|
||||
'StartsWithTest' => 'Does not start with test',
|
||||
'GreaterThan100' => 99,
|
||||
'LessThan100' => 99,
|
||||
'SomeField' => 'Some Value',
|
||||
],
|
||||
[
|
||||
'ID' => 4,
|
||||
'Name' => 'Clair',
|
||||
'Age' => 21,
|
||||
'Title' => 'Fourth Object',
|
||||
'StartsWithTest' => 'test value, but lower case',
|
||||
'GreaterThan100' => 100,
|
||||
'LessThan100' => 100,
|
||||
'SomeField' => 'some value',
|
||||
],
|
||||
[
|
||||
'ID' => 5,
|
||||
'Name' => 'Clair',
|
||||
'Age' => 52,
|
||||
'Title' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function provideFilterWithSearchfilters()
|
||||
{
|
||||
// Note that search filter tests here are to test syntax and to ensure all supported search filters
|
||||
// work with arraylist - but we don't need to test every possible edge case here,
|
||||
// we can rely on individual searchfilter unit tests for many edge cases
|
||||
$objects = $this->getFilterWithSearchfiltersObjects();
|
||||
return [
|
||||
// exact match filter tests
|
||||
'exact match - negate' => [
|
||||
'args' => ['Title:not', 'First Object'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1], $objects[2], $objects[3], $objects[4]],
|
||||
],
|
||||
'exact match - negate two different ways' => [
|
||||
'args' => [[
|
||||
'Title:not' => 'First Object',
|
||||
'Title:ExactMatch:not' => 'Third Object',
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1], $objects[3], $objects[4]],
|
||||
],
|
||||
'exact match negated - nothing gets filtered out' => [
|
||||
'filter' => ['Title:not', 'No object has this title - we should have all objects'],
|
||||
'objects' => $objects,
|
||||
'expected' => $objects,
|
||||
],
|
||||
'exact match negated against null - only last item gets filtered out' => [
|
||||
'args' => ['SomeField:not', null],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[2], $objects[3]],
|
||||
],
|
||||
'exact match with a few items' => [
|
||||
'args' => ['Title', ['First Object', 'Second Object', 'Third Object']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[2]],
|
||||
],
|
||||
'negate the above test' => [
|
||||
'args' => ['Title:not', ['First Object', 'Second Object', 'Third Object']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[3], $objects[4]],
|
||||
],
|
||||
// case sensitivity checks
|
||||
'exact match case sensitive' => [
|
||||
'args' => [['NoCase' => 'case sensitive']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1]],
|
||||
],
|
||||
'exact match case insensitive' => [
|
||||
'args' => ['NoCase:nocase', 'case sensitive'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1]],
|
||||
],
|
||||
'exact match mixed case filters' => [
|
||||
'args' => [[
|
||||
'NoCase:nocase' => 'case sensitive',
|
||||
'CaseSensitive' => 'case sensitive',
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1]],
|
||||
],
|
||||
// explicit exact match
|
||||
'exact match explicit' => [
|
||||
'args' => ['Title:ExactMatch', 'Third Object'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2]],
|
||||
],
|
||||
'exact match explicit with modifier' => [
|
||||
'args' => [['Title:ExactMatch:nocase' => 'third object']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2]],
|
||||
],
|
||||
// partialmatch filter
|
||||
'partial match' => [
|
||||
'args' => ['StartsWithTest:PartialMatch', 'start'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2]],
|
||||
],
|
||||
'partial match with modifier' => [
|
||||
'args' => [['StartsWithTest:PartialMatch:nocase' => 'start']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1], $objects[2]],
|
||||
],
|
||||
// greaterthan filter
|
||||
'greaterthan match' => [
|
||||
'args' => ['GreaterThan100:GreaterThan', 100],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1]],
|
||||
],
|
||||
'greaterthan match with modifier' => [
|
||||
'args' => [['GreaterThan100:GreaterThan:not' => 100]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2], $objects[3], $objects[4]],
|
||||
],
|
||||
// greaterthanorequal filter
|
||||
'greaterthanorequal match' => [
|
||||
'args' => ['GreaterThan100:GreaterThanOrEqual', 100],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[3]],
|
||||
],
|
||||
'greaterthanorequal match with modifier' => [
|
||||
'args' => [['GreaterThan100:GreaterThanOrEqual:not' => 100]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2], $objects[4]],
|
||||
],
|
||||
// lessthan filter
|
||||
'lessthan match' => [
|
||||
'args' => ['LessThan100:LessThan', 100],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[2], $objects[4]],
|
||||
],
|
||||
'lessthan match with modifier' => [
|
||||
'args' => [['LessThan100:LessThan:not' => 100]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[3]],
|
||||
],
|
||||
// lessthanorequal filter
|
||||
'lessthanorequal match' => [
|
||||
'args' => ['LessThan100:LessThanOrEqual', 99],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[2], $objects[4]],
|
||||
],
|
||||
'lessthanorequal match with modifier' => [
|
||||
'args' => [['LessThan100:LessThanOrEqual:not' => 99]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[3]],
|
||||
],
|
||||
// various more complex filters/combinations and extra scenarios
|
||||
'complex1' => [
|
||||
'args' => [[
|
||||
'NoCase:nocase' => 'CASE SENSITIVE',
|
||||
'StartsWithTest:StartsWith' => 'Not',
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1]],
|
||||
],
|
||||
'complex2' => [
|
||||
'args' => [[
|
||||
'NoCase:case' => 'CASE SENSITIVE',
|
||||
'StartsWithTest:StartsWith' => 'Not',
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [],
|
||||
],
|
||||
'complex3' => [
|
||||
'args' => [[
|
||||
'LessThan100:LessThan' => 100,
|
||||
'GreaterThan100:GreaterThan:not' => 100,
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2], $objects[4]],
|
||||
],
|
||||
'complex4' => [
|
||||
'args' => [[
|
||||
'LessThan100:LessThan' => 1,
|
||||
'GreaterThan100:GreaterThan' => 100,
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideFilterWithSearchfilters
|
||||
*/
|
||||
public function testFilterWithSearchfilters(array $args, array $objects, array $expected)
|
||||
{
|
||||
$list = new ArrayList($objects);
|
||||
$list = $list->filter(...$args);
|
||||
$this->assertEquals(array_column($expected, 'ID'), $list->column('ID'));
|
||||
}
|
||||
|
||||
public function provideFilterAnyWithSearchfilters()
|
||||
{
|
||||
$objects = $this->getFilterWithSearchfiltersObjects();
|
||||
return [
|
||||
// test a couple of search filters
|
||||
// don't need to be as explicit as the filter tests, just check the syntax works
|
||||
'partial match' => [
|
||||
'args' => ['StartsWithTest:PartialMatch', 'start'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2]],
|
||||
],
|
||||
'partial match with modifier' => [
|
||||
'args' => ['StartsWithTest:PartialMatch:nocase', 'start'],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1], $objects[2]],
|
||||
],
|
||||
'greaterthan match' => [
|
||||
'args' => ['GreaterThan100:GreaterThan', 100],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1]],
|
||||
],
|
||||
'greaterthan match with modifier' => [
|
||||
'args' => ['GreaterThan100:GreaterThan:not', 100],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[2], $objects[3], $objects[4]],
|
||||
],
|
||||
'multiple filters match' => [
|
||||
'args' => [[
|
||||
'StartsWithTest:PartialMatch:nocase' => 'start',
|
||||
'Age:GreaterThanOrEqual' => 43,
|
||||
]],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[1], $objects[2], $objects[4]],
|
||||
],
|
||||
'partial match with a few items' => [
|
||||
'args' => ['Title:PartialMatch', ['First Object', 'Second Object', 'Third Object']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[0], $objects[1], $objects[2]],
|
||||
],
|
||||
'negate the above test' => [
|
||||
'args' => ['Title:PartialMatch:not', ['First Object', 'Second Object', 'Third Object']],
|
||||
'objects' => $objects,
|
||||
'expected' => [$objects[3], $objects[4]],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideFilterAnyWithSearchfilters
|
||||
*/
|
||||
public function testFilterAnyWithSearchfilters(array $args, array $objects, array $expected)
|
||||
{
|
||||
$list = new ArrayList($objects);
|
||||
$list = $list->filterAny(...$args);
|
||||
$this->assertEquals(array_column($expected, 'ID'), $list->column('ID'));
|
||||
}
|
||||
|
||||
public function provideFilterAny()
|
||||
{
|
||||
$list = new ArrayList(
|
||||
[
|
||||
$steve = ['Name' => 'Steve', 'ID' => 1, 'Age' => 21],
|
||||
$bob = ['Name' => 'Bob', 'ID' => 2, 'Age' => 18],
|
||||
$clair = ['Name' => 'Clair', 'ID' => 3, 'Age' => 21],
|
||||
$phil = ['Name' => 'Phil', 'ID' => 4, 'Age' => 21],
|
||||
$oscar = ['Name' => 'Oscar', 'ID' => 5, 'Age' => 52],
|
||||
$mike = ['Name' => 'Mike', 'ID' => 6, 'Age' => 43],
|
||||
$steve = ['Name' => 'Steve', 'ID' => 1, 'Age' => 21],
|
||||
$bob = ['Name' => 'Bob', 'ID' => 2, 'Age' => 18],
|
||||
$clair = ['Name' => 'Clair', 'ID' => 3, 'Age' => 21],
|
||||
$phil = ['Name' => 'Phil', 'ID' => 4, 'Age' => 21],
|
||||
$oscar = ['Name' => 'Oscar', 'ID' => 5, 'Age' => 52],
|
||||
$mike = ['Name' => 'Mike', 'ID' => 6, 'Age' => 43],
|
||||
]
|
||||
);
|
||||
return [
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => ['Name', 'Bob'],
|
||||
'contains' => [$bob],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => ['Name', ['Aziz', 'Bob']],
|
||||
'contains' => [$bob],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => ['Name', ['Steve', 'Bob']],
|
||||
'contains' => [$steve, $bob],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => [['Name' => 'Bob', 'Age' => 21]],
|
||||
'contains' => [$bob, $steve, $clair, $phil],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => [['Name' => 'Bob', 'Age' => [21, 43]]],
|
||||
'contains' => [$bob, $steve, $clair, $mike, $phil],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => [['Name' => ['Bob', 'Phil'], 'Age' => [21, 43]]],
|
||||
'contains' => [$bob, $steve, $clair, $mike, $phil],
|
||||
],
|
||||
[
|
||||
'list' => $list,
|
||||
'args' => [['Name' => ['Bob', 'Nobody'], 'Age' => [21, 43]]],
|
||||
'contains' => [$bob, $steve, $clair, $mike, $phil],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// only bob in the list
|
||||
//$list = $list->filterAny('Name', 'bob');
|
||||
$filteredList = $list->filterAny('Name', 'Bob')->toArray();
|
||||
$this->assertCount(1, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
|
||||
// azis or bob in the list
|
||||
//$list = $list->filterAny('Name', ['aziz', 'bob']);
|
||||
$filteredList = $list->filterAny('Name', ['Aziz', 'Bob'])->toArray();
|
||||
$this->assertCount(1, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
|
||||
$filteredList = $list->filterAny('Name', ['Steve', 'Bob'])->toArray();
|
||||
$this->assertCount(2, $filteredList);
|
||||
$this->assertContains($steve, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
|
||||
// bob or anyone aged 21 in the list
|
||||
//$list = $list->filterAny(['Name'=>'bob, 'Age'=>21]);
|
||||
$filteredList = $list->filterAny(['Name' => 'Bob', 'Age' => 21])->toArray();
|
||||
$this->assertCount(4, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
$this->assertContains($steve, $filteredList);
|
||||
$this->assertContains($clair, $filteredList);
|
||||
$this->assertContains($phil, $filteredList);
|
||||
|
||||
// bob or anyone aged 21 or 43 in the list
|
||||
// $list = $list->filterAny(['Name'=>'bob, 'Age'=>[21, 43]]);
|
||||
$filteredList = $list->filterAny(['Name' => 'Bob', 'Age' => [21, 43]])->toArray();
|
||||
$this->assertCount(5, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
$this->assertContains($steve, $filteredList);
|
||||
$this->assertContains($clair, $filteredList);
|
||||
$this->assertContains($mike, $filteredList);
|
||||
$this->assertContains($phil, $filteredList);
|
||||
|
||||
// all bobs, phils or anyone aged 21 or 43 in the list
|
||||
//$list = $list->filterAny(['Name'=>['bob','phil'], 'Age'=>[21, 43]]);
|
||||
$filteredList = $list->filterAny(['Name' => ['Bob', 'Phil'], 'Age' => [21, 43]])->toArray();
|
||||
$this->assertCount(5, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
$this->assertContains($steve, $filteredList);
|
||||
$this->assertContains($clair, $filteredList);
|
||||
$this->assertContains($mike, $filteredList);
|
||||
$this->assertContains($phil, $filteredList);
|
||||
|
||||
$filteredList = $list->filterAny(['Name' => ['Bob', 'Nobody'], 'Age' => [21, 43]])->toArray();
|
||||
$this->assertCount(5, $filteredList);
|
||||
$this->assertContains($bob, $filteredList);
|
||||
$this->assertContains($steve, $filteredList);
|
||||
$this->assertContains($clair, $filteredList);
|
||||
$this->assertContains($mike, $filteredList);
|
||||
$this->assertContains($phil, $filteredList);
|
||||
/**
|
||||
* @dataProvider provideFilterAny
|
||||
*/
|
||||
public function testFilterAny(ArrayList $list, array $args, array $contains)
|
||||
{
|
||||
$filteredList = $list->filterAny(...$args)->toArray();
|
||||
$this->assertCount(count($contains), $filteredList);
|
||||
foreach ($contains as $item) {
|
||||
$this->assertContains($item, $filteredList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1195,6 +1529,106 @@ class ArrayListTest extends SapphireTest
|
||||
$this->assertEquals($expected, $list->toArray());
|
||||
}
|
||||
|
||||
public function provideExcludeWithSearchfilters()
|
||||
{
|
||||
// If it's included in the filter test, then it's excluded in the exclude test,
|
||||
// so we can just use the same scenarios and reverse the expected results.
|
||||
$objects = $this->getFilterWithSearchfiltersObjects();
|
||||
$scenarios = $this->provideFilterWithSearchfilters();
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$kept = [];
|
||||
$excluded = [];
|
||||
foreach ($scenario['expected'] as $item) {
|
||||
$kept[] = $item['ID'];
|
||||
}
|
||||
foreach ($objects as $item) {
|
||||
if (!in_array($item['ID'], $kept)) {
|
||||
$excluded[] = $item;
|
||||
}
|
||||
}
|
||||
$scenarios[$name]['expected'] = $excluded;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExcludeWithSearchfilters
|
||||
*/
|
||||
public function testExcludeWithSearchfilters(array $args, array $objects, array $expected)
|
||||
{
|
||||
$list = new ArrayList($objects);
|
||||
$list = $list->exclude(...$args);
|
||||
$this->assertEquals($expected, $list->toArray());
|
||||
}
|
||||
|
||||
public function provideExcludeAnyWithSearchfilters()
|
||||
{
|
||||
// If it's included in the filterAny test, then it's excluded in the excludeAny test,
|
||||
// so we can just use the same scenarios and reverse the expected results.
|
||||
$objects = $this->getFilterWithSearchfiltersObjects();
|
||||
$scenarios = $this->provideFilterAnyWithSearchfilters();
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$kept = [];
|
||||
$excluded = [];
|
||||
foreach ($scenario['expected'] as $item) {
|
||||
$kept[] = $item['ID'];
|
||||
}
|
||||
foreach ($objects as $item) {
|
||||
if (!in_array($item['ID'], $kept)) {
|
||||
$excluded[] = $item;
|
||||
}
|
||||
}
|
||||
$scenarios[$name]['expected'] = $excluded;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExcludeAnyWithSearchfilters
|
||||
*/
|
||||
public function testExcludeAnyWithSearchfilters(array $args, array $objects, array $expected)
|
||||
{
|
||||
$list = new ArrayList($objects);
|
||||
$list = $list->excludeAny(...$args);
|
||||
$this->assertEquals($expected, $list->toArray());
|
||||
}
|
||||
|
||||
public function provideExcludeAny()
|
||||
{
|
||||
// If it's included in the filterAny test, then it's excluded in the excludeAny test,
|
||||
// so we can just use the same scenarios and reverse the expected results.
|
||||
$scenarios = $this->provideFilterAny();
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$kept = [];
|
||||
$excluded = [];
|
||||
/** @var array $item */
|
||||
foreach ($scenario['contains'] as $item) {
|
||||
$kept[] = $item['ID'];
|
||||
}
|
||||
/** @var ArrayData $item */
|
||||
foreach ($scenario['list'] as $item) {
|
||||
$itemAsArray = $item->toMap();
|
||||
if (!in_array($itemAsArray['ID'], $kept)) {
|
||||
$excluded[] = $itemAsArray;
|
||||
}
|
||||
}
|
||||
$scenarios[$name]['contains'] = $excluded;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExcludeAny
|
||||
*/
|
||||
public function testExcludeAny(ArrayList $list, array $args, array $contains)
|
||||
{
|
||||
$filteredList = $list->excludeAny(...$args)->toArray();
|
||||
$this->assertCount(count($contains), $filteredList);
|
||||
foreach ($contains as $item) {
|
||||
$this->assertContains($item, $filteredList);
|
||||
}
|
||||
}
|
||||
|
||||
public function testCanFilterBy()
|
||||
{
|
||||
$list = new ArrayList(
|
||||
|
@ -31,6 +31,7 @@ use SilverStripe\ORM\Connect\DatabaseException;
|
||||
use SilverStripe\ORM\FieldType\DBPrimaryKey;
|
||||
use SilverStripe\ORM\FieldType\DBText;
|
||||
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||
use SilverStripe\ORM\Filters\SearchFilter;
|
||||
use SilverStripe\ORM\Tests\DataObjectTest\RelationChildFirst;
|
||||
use SilverStripe\ORM\Tests\DataObjectTest\RelationChildSecond;
|
||||
|
||||
@ -88,6 +89,62 @@ class DataListTest extends SapphireTest
|
||||
$this->assertEquals(2, count($list ?? []));
|
||||
}
|
||||
|
||||
public function provideDefaultCaseSensitivity()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'caseSensitive' => true,
|
||||
'filter' => ['FirstName' => 'captain'],
|
||||
'expectedCount' => 0,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => false,
|
||||
'filter' => ['FirstName' => 'captain'],
|
||||
'expectedCount' => 1,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => true,
|
||||
'filter' => ['FirstName:PartialMatch' => 'captain'],
|
||||
'expectedCount' => 0,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => false,
|
||||
'filter' => ['FirstName:PartialMatch' => 'captain'],
|
||||
'expectedCount' => 2,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => true,
|
||||
'filter' => ['FirstName:StartsWith' => 'captain'],
|
||||
'expectedCount' => 0,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => false,
|
||||
'filter' => ['FirstName:StartsWith' => 'captain'],
|
||||
'expectedCount' => 2,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => true,
|
||||
'filter' => ['Surname:EndsWith' => 'Keeper'],
|
||||
'expectedCount' => 0,
|
||||
],
|
||||
[
|
||||
'caseSensitive' => false,
|
||||
'filter' => ['Surname:EndsWith' => 'Keeper'],
|
||||
'expectedCount' => 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDefaultCaseSensitivity
|
||||
*/
|
||||
public function testDefaultCaseSensitivity(bool $caseSensitive, array $filter, int $expectedCount)
|
||||
{
|
||||
SearchFilter::config()->set('default_case_sensitive', $caseSensitive);
|
||||
$list = Player::get()->filter($filter);
|
||||
$this->assertCount($expectedCount, $list);
|
||||
}
|
||||
|
||||
public function testCount()
|
||||
{
|
||||
$list = new DataList(Team::class);
|
||||
|
@ -49,16 +49,19 @@ class EagerLoadedListTest extends SapphireTest
|
||||
'ID' => 1,
|
||||
'Name' => 'test obj 1',
|
||||
'Created' => '2013-01-01 00:00:00',
|
||||
'SomeField' => 'VaLuE',
|
||||
],
|
||||
[
|
||||
'ID' => 2,
|
||||
'Name' => 'test obj 2',
|
||||
'Created' => '2023-01-01 00:00:00',
|
||||
'SomeField' => 'value',
|
||||
],
|
||||
[
|
||||
'ID' => 3,
|
||||
'Name' => 'test obj 3',
|
||||
'Created' => '2023-01-01 00:00:00',
|
||||
'SomeField' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
@ -316,6 +319,7 @@ class EagerLoadedListTest extends SapphireTest
|
||||
|
||||
/**
|
||||
* @dataProvider provideFilter
|
||||
* @dataProvider provideFilterWithSearchFilters
|
||||
*/
|
||||
public function testFilter(
|
||||
string $dataListClass,
|
||||
@ -417,7 +421,6 @@ class EagerLoadedListTest extends SapphireTest
|
||||
'dataListClass' => ManyManyThroughList::class,
|
||||
'eagerloadedDataClass' => ValidatedObject::class,
|
||||
$rows,
|
||||
// Filter by ID is handled slightly differently than other fields
|
||||
'filter' => [
|
||||
'ID' => [1, 2],
|
||||
],
|
||||
@ -426,6 +429,284 @@ class EagerLoadedListTest extends SapphireTest
|
||||
];
|
||||
}
|
||||
|
||||
public function provideFilterWithSearchFilters()
|
||||
{
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$scenarios = [
|
||||
// exact match filter tests
|
||||
'exact match - negate' => [
|
||||
'filter' => ['Name:not' => 'test obj 1'],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
'exact match - negate two different ways' => [
|
||||
'filter' => [
|
||||
'Name:not' => 'test obj 1',
|
||||
'Name:ExactMatch:not' => 'test obj 3',
|
||||
],
|
||||
'expected' => [2],
|
||||
],
|
||||
'exact match negated - nothing gets filtered out' => [
|
||||
'filter' => ['Name:not' => 'No row has this name - we should have all rows'],
|
||||
'expected' => array_column($rows, 'ID'),
|
||||
],
|
||||
'exact match negated against null - only last item gets filtered out' => [
|
||||
'filter' => ['SomeField:not' => null],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'exact match negated with a few items' => [
|
||||
'filter' => [
|
||||
'Name:not' => ['test obj 1', 'test obj 3', 'not there'],
|
||||
],
|
||||
'expected' => [2],
|
||||
],
|
||||
// case sensitivity checks
|
||||
'exact match case sensitive' => [
|
||||
'filter' => ['SomeField:case' => 'value'],
|
||||
'expected' => [2],
|
||||
],
|
||||
'exact match case insensitive' => [
|
||||
'filter' => ['SomeField:nocase' => 'value'],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
// explicit exact match
|
||||
'exact match explicit' => [
|
||||
'filter' => ['Name:ExactMatch' => 'test obj 2'],
|
||||
'expected' => [2],
|
||||
],
|
||||
'exact match explicit with modifier' => [
|
||||
'filter' => ['Name:ExactMatch:nocase' => 'Test Obj 2'],
|
||||
'expected' => [2],
|
||||
],
|
||||
// partialmatch filter
|
||||
'partial match' => [
|
||||
'filter' => ['SomeField:PartialMatch:case' => 'alu'],
|
||||
'expected' => [2],
|
||||
],
|
||||
'partial match with modifier' => [
|
||||
'filter' => ['SomeField:PartialMatch:nocase' => 'alu'],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
// greaterthan filter
|
||||
'greaterthan match' => [
|
||||
'filter' => ['ID:GreaterThan' => 2],
|
||||
'expected' => [3],
|
||||
],
|
||||
'greaterthan match with modifier' => [
|
||||
'filter' => ['ID:GreaterThan:not' => 2],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
// greaterthanorequal filter
|
||||
'greaterthanorequal match' => [
|
||||
'filter' => ['ID:GreaterThanOrEqual' => 2],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
'greaterthanorequal match with modifier' => [
|
||||
'filter' => ['ID:GreaterThanOrEqual:not' => 2],
|
||||
'expected' => [1],
|
||||
],
|
||||
// lessthan filter
|
||||
'lessthan match' => [
|
||||
'filter' => ['ID:LessThan' => 2],
|
||||
'expected' => [1],
|
||||
],
|
||||
'lessthan match with modifier' => [
|
||||
'filter' => ['ID:LessThan:not' => 2],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
// lessthanorequal filter
|
||||
'lessthanorequal match' => [
|
||||
'filter' => ['ID:LessThanOrEqual' => 2],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'lessthanorequal match with modifier' => [
|
||||
'filter' => ['ID:LessThanOrEqual:not' => 2],
|
||||
'expected' => [3],
|
||||
],
|
||||
// various more complex filters/combinations and extra scenarios
|
||||
'complex1' => [
|
||||
'filter' => [
|
||||
'SomeField:nocase' => 'value',
|
||||
'Name:StartsWith' => 'test',
|
||||
],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'complex2' => [
|
||||
'filter' => [
|
||||
'ID:LessThan' => 3,
|
||||
'ID:GreaterThan:not' => 1,
|
||||
],
|
||||
'expected' => [1],
|
||||
],
|
||||
'complex3' => [
|
||||
'filter' => [
|
||||
'ID:LessThan' => 3,
|
||||
'ID:GreaterThan' => 1,
|
||||
],
|
||||
'expected' => [2],
|
||||
],
|
||||
];
|
||||
// No need to vary these between scenarios, we're just checking search filter
|
||||
// syntax works as expected.
|
||||
foreach (array_keys($scenarios) as $key) {
|
||||
array_unshift($scenarios[$key], $rows);
|
||||
array_unshift($scenarios[$key], ValidatedObject::class);
|
||||
array_unshift($scenarios[$key], DataList::class);
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideFilterAnyWithSearchFilters
|
||||
*/
|
||||
public function testFilterAnyWithSearchfilters(array $filter, array $expectedIDs): void
|
||||
{
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$list = new EagerLoadedList(ValidatedObject::class, DataList::class);
|
||||
foreach ($rows as $row) {
|
||||
$list->addRow($row);
|
||||
}
|
||||
$filteredList = $list->filterAny($filter);
|
||||
|
||||
// Validate that the unfiltered list still has all records, and the filtered list has the expected amount
|
||||
$this->assertCount(count($rows), $list);
|
||||
$this->assertCount(count($expectedIDs), $filteredList);
|
||||
|
||||
// Validate that the filtered list has the CORRECT records
|
||||
$this->iterate($list, $rows, array_column($rows, 'ID'));
|
||||
}
|
||||
|
||||
public function provideFilterAnyWithSearchFilters()
|
||||
{
|
||||
return [
|
||||
// test a couple of search filters
|
||||
// don't need to be as explicit as the filter tests, just check the syntax works
|
||||
'partial match' => [
|
||||
'filter' => ['Name:PartialMatch' => 'test obj'],
|
||||
'expected' => [1, 2, 3],
|
||||
],
|
||||
'partial match2' => [
|
||||
'filter' => ['Name:PartialMatch' => 3],
|
||||
'expected' => [3],
|
||||
],
|
||||
'partial match with modifier' => [
|
||||
'filter' => ['SomeField:PartialMatch:nocase' => 'alu'],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'greaterthan match' => [
|
||||
'filter' => ['ID:GreaterThan'=> 2],
|
||||
'expected' => [3],
|
||||
],
|
||||
'greaterthan match with modifier' => [
|
||||
'filter' => ['ID:GreaterThan:not' => 2],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'multiple filters match' => [
|
||||
'filter' => [
|
||||
'SomeField:PartialMatch:case' => 'val',
|
||||
'ID:GreaterThanOrEqual' => 2,
|
||||
],
|
||||
'expected' => [2, 3],
|
||||
],
|
||||
'exact match with a few items' => [
|
||||
'filter' => ['Name:ExactMatch' => ['test obj 1', 'test obj 2']],
|
||||
'expected' => [1, 2],
|
||||
],
|
||||
'negate the above test' => [
|
||||
'filter' => ['Name:ExactMatch:not' => ['test obj 1', 'test obj 2']],
|
||||
'expected' => [3],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function provideExcludeWithSearchfilters()
|
||||
{
|
||||
// If it's included in the filter test, then it's excluded in the exclude test,
|
||||
// so we can just use the same scenarios and reverse the expected results.
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$scenarios = $this->provideFilterWithSearchfilters();
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$kept = [];
|
||||
$excluded = [];
|
||||
foreach ($scenario['expected'] as $id) {
|
||||
$kept[] = $id;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
if (!in_array($row['ID'], $kept)) {
|
||||
$excluded[] = $row['ID'];
|
||||
}
|
||||
}
|
||||
$scenarios[$name]['expected'] = $excluded;
|
||||
|
||||
// Remove args we won't be using for this test
|
||||
foreach (['dataListClass', 'eagerloadedDataClass', 'rows'] as $removeFromScenario) {
|
||||
array_shift($scenarios[$name]);
|
||||
}
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExcludeWithSearchfilters
|
||||
*/
|
||||
public function testExcludeWithSearchfilters(array $filter, array $expectedIDs): void
|
||||
{
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$list = new EagerLoadedList(ValidatedObject::class, DataList::class);
|
||||
foreach ($rows as $row) {
|
||||
$list->addRow($row);
|
||||
}
|
||||
$filteredList = $list->exclude($filter);
|
||||
|
||||
// Validate that the unfiltered list still has all records, and the filtered list has the expected amount
|
||||
$this->assertCount(count($rows), $list);
|
||||
$this->assertCount(count($expectedIDs), $filteredList);
|
||||
|
||||
// Validate that the filtered list has the CORRECT records
|
||||
$this->iterate($list, $rows, array_column($rows, 'ID'));
|
||||
}
|
||||
|
||||
public function provideExcludeAnyWithSearchfilters()
|
||||
{
|
||||
// If it's included in the filterAny test, then it's excluded in the excludeAny test,
|
||||
// so we can just use the same scenarios and reverse the expected results.
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$scenarios = $this->provideFilterAnyWithSearchfilters();
|
||||
foreach ($scenarios as $name => $scenario) {
|
||||
$kept = [];
|
||||
$excluded = [];
|
||||
foreach ($scenario['expected'] as $id) {
|
||||
$kept[] = $id;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
if (!in_array($row['ID'], $kept)) {
|
||||
$excluded[] = $row['ID'];
|
||||
}
|
||||
}
|
||||
$scenarios[$name]['expected'] = $excluded;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExcludeAnyWithSearchfilters
|
||||
*/
|
||||
public function testExcludeAnyWithSearchfilters(array $filter, array $expectedIDs): void
|
||||
{
|
||||
$rows = $this->getBasicRecordRows();
|
||||
$list = new EagerLoadedList(ValidatedObject::class, DataList::class);
|
||||
foreach ($rows as $row) {
|
||||
$list->addRow($row);
|
||||
}
|
||||
$filteredList = $list->excludeAny($filter);
|
||||
|
||||
// Validate that the unfiltered list still has all records, and the filtered list has the expected amount
|
||||
$this->assertCount(count($rows), $list);
|
||||
$this->assertCount(count($expectedIDs), $filteredList);
|
||||
|
||||
// Validate that the filtered list has the CORRECT records
|
||||
$this->iterate($list, $rows, array_column($rows, 'ID'));
|
||||
}
|
||||
|
||||
public function testFilterByInvalidColumn()
|
||||
{
|
||||
$list = new EagerLoadedList(ValidatedObject::class, DataList::class);
|
||||
@ -1189,6 +1470,23 @@ class EagerLoadedListTest extends SapphireTest
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by non-null' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email:not' => null],
|
||||
'expected' => [
|
||||
[
|
||||
'Name' => 'Damian',
|
||||
'Email' => 'damian@thefans.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Richard',
|
||||
'Email' => 'richie@richers.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Hamish',
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by empty only' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email' => ''],
|
||||
@ -1198,6 +1496,27 @@ class EagerLoadedListTest extends SapphireTest
|
||||
]
|
||||
],
|
||||
],
|
||||
// This should include null values, matching the behaviour in DataList
|
||||
'Non-empty only' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email:not' => ''],
|
||||
'expected' => [
|
||||
[
|
||||
'Name' => 'Damian',
|
||||
'Email' => 'damian@thefans.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Richard',
|
||||
'Email' => 'richie@richers.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Stephen',
|
||||
],
|
||||
[
|
||||
'Name' => 'Mitch',
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by null or empty values' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email' => [null, '']],
|
||||
@ -1232,7 +1551,17 @@ class EagerLoadedListTest extends SapphireTest
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by many including empty string and non-empty' => [
|
||||
'Filter exclusion of above list' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email:not' => [null, '', 'damian@thefans.com']],
|
||||
'expected' => [
|
||||
[
|
||||
'Name' => 'Richard',
|
||||
'Email' => 'richie@richers.com',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Filter by many including empty string and non-empty 1' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email' => ['', 'damian@thefans.com']],
|
||||
'expected' => [
|
||||
@ -1245,6 +1574,41 @@ class EagerLoadedListTest extends SapphireTest
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by many including empty string and non-empty 2' => [
|
||||
'filterMethod' => 'filter',
|
||||
'filter' => ['Email:not' => ['', 'damian@thefans.com']],
|
||||
'expected' => [
|
||||
[
|
||||
'Name' => 'Richard',
|
||||
'Email' => 'richie@richers.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Stephen',
|
||||
],
|
||||
[
|
||||
'Name' => 'Mitch',
|
||||
]
|
||||
],
|
||||
],
|
||||
'Filter by many including empty string and non-empty 3' => [
|
||||
'filterMethod' => 'filterAny',
|
||||
'filter' => [
|
||||
'Email:not' => ['', 'damian@thefans.com'],
|
||||
'Email' => null
|
||||
],
|
||||
'expected' => [
|
||||
[
|
||||
'Name' => 'Richard',
|
||||
'Email' => 'richie@richers.com',
|
||||
],
|
||||
[
|
||||
'Name' => 'Stephen',
|
||||
],
|
||||
[
|
||||
'Name' => 'Mitch',
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1390,6 +1754,16 @@ class EagerLoadedListTest extends SapphireTest
|
||||
];
|
||||
}
|
||||
|
||||
public function testExcludeWithSearchFilter()
|
||||
{
|
||||
$list = $this->getListWithRecords(TeamComment::class);
|
||||
$list = $list->exclude('Comment:PartialMatch', 'Bob');
|
||||
$this->assertListEquals([
|
||||
['Name' => 'Joe'],
|
||||
['Name' => 'Phil'],
|
||||
], $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that Bob and Phil are excluded (one match each)
|
||||
*/
|
||||
|
277
tests/php/ORM/Filters/EndsWithFilterTest.php
Normal file
277
tests/php/ORM/Filters/EndsWithFilterTest.php
Normal file
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\EndsWithFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class EndsWithFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
'null ends with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty ends with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'null ends with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty ends with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty ends with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'true doesnt end with empty' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt end with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt end with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'null ends with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'false doesnt end with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt end with true' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt end with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt end with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt end with true' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'number is cast to string' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'1 ends with 1' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'100 doesnt end with 1' => [
|
||||
'filterValue' => '1',
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'100 ends with 0' => [
|
||||
'filterValue' => '0',
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'100 still ends with 0' => [
|
||||
'filterValue' => 0,
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'SomeValue ends with SomeValue' => [
|
||||
'filterValue' => 'SomeValue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'SomeValue doesnt end with somevalue' => [
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
'SomeValue doesnt end with meVal' => [
|
||||
'filterValue' => 'meVal',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'SomeValue ends with Value' => [
|
||||
'filterValue' => 'Value',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'SomeValue doesnt with vAlUe' => [
|
||||
'filterValue' => 'vAlUe',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
// unicode matches
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, 'somevalue', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'Value', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'meVal', 'abc'],
|
||||
'objValue' => 'Some',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// These will both evaluate to true because the __toString() method just returns the class name.
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// case insensitive
|
||||
[
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'vAlUe',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'meval',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'different',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = $scenario['matches'] === null ? null : !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
// explicit case sensitive
|
||||
foreach ($scenarios as $scenario) {
|
||||
if (!in_array('nocase', $scenario['modifiers'])) {
|
||||
$scenario['modifiers'][] = 'case';
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, ?bool $matches)
|
||||
{
|
||||
// Test with explicit default case sensitivity rather than relying on the collation, so that database
|
||||
// settings don't interfere with the test
|
||||
foreach ([true, false] as $caseSensitive) {
|
||||
// Handle cases where the expected value can depend on the default case sensitivity
|
||||
if ($matches === null) {
|
||||
$nullMatch = !(in_array('case', $modifiers) ?: $caseSensitive);
|
||||
if (in_array('not', $modifiers)) {
|
||||
$nullMatch = !$nullMatch;
|
||||
}
|
||||
}
|
||||
|
||||
EndsWithFilter::config()->set('default_case_sensitive', $caseSensitive);
|
||||
$filter = new EndsWithFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches ?? $nullMatch, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ use SilverStripe\ORM\Filters\ExactMatchFilter;
|
||||
use SilverStripe\ORM\Tests\Filters\ExactMatchFilterTest\Task;
|
||||
use SilverStripe\ORM\Tests\Filters\ExactMatchFilterTest\Project;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class ExactMatchFilterTest extends SapphireTest
|
||||
{
|
||||
@ -93,4 +94,230 @@ class ExactMatchFilterTest extends SapphireTest
|
||||
$titleQueryUsesPlaceholders = isset($matches[1]) ? $matches[1] === '?, ?, ?' : null;
|
||||
return [$idQueryUsesPlaceholders, $titleQueryUsesPlaceholders];
|
||||
}
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
[
|
||||
'filterValue' => null,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'objValue' => 'Some',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'objValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// unicode matches
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, 'somevalue', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'SomeValue', 'abc'],
|
||||
'objValue' => 'Some',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => [1, 2, 3],
|
||||
'objValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => [4, 5, 6],
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// test something that is clearly not strings, since exact match
|
||||
// is the default for ArrayList filtering which can have basically
|
||||
// anything as its value
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// case insensitive
|
||||
[
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
// doesn't do partial matching even when case insensitive
|
||||
[
|
||||
'filterValue' => 'some',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = $scenario['matches'] === null ? null : !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
// explicitly case sensitive
|
||||
foreach ($scenarios as $scenario) {
|
||||
if (!in_array('nocase', $scenario['modifiers'])) {
|
||||
$scenario['modifiers'][] = 'case';
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $objValue, array $modifiers, ?bool $matches)
|
||||
{
|
||||
// Test with explicit default case sensitivity rather than relying on the collation, so that database
|
||||
// settings don't interfere with the test
|
||||
foreach ([true, false] as $caseSensitive) {
|
||||
// Handle cases where the expected value can depend on the default case sensitivity
|
||||
if ($matches === null) {
|
||||
$nullMatch = !(in_array('case', $modifiers) ?: $caseSensitive);
|
||||
if (in_array('not', $modifiers)) {
|
||||
$nullMatch = !$nullMatch;
|
||||
}
|
||||
}
|
||||
|
||||
ExactMatchFilter::config()->set('default_case_sensitive', $caseSensitive);
|
||||
$filter = new ExactMatchFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches ?? $nullMatch, $filter->matches($objValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
225
tests/php/ORM/Filters/GreaterThanFilterTest.php
Normal file
225
tests/php/ORM/Filters/GreaterThanFilterTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\GreaterThanFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GreaterThanFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'somevalue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '2',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '12',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 12,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// unicode matches - macrons are "greater" than their non-macron equivalent
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, '99', '123456'],
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, '0', '123456'],
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, bool $matches)
|
||||
{
|
||||
$filter = new GreaterThanFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
225
tests/php/ORM/Filters/GreaterThanOrEqualFilterTest.php
Normal file
225
tests/php/ORM/Filters/GreaterThanOrEqualFilterTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\GreaterThanOrEqualFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GreaterThanOrEqualFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'somevalue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '2',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '12',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 12,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// unicode matches - macrons are "greater" than their non-macron equivalent
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, '99', '123456'],
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, '0', '123456'],
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, bool $matches)
|
||||
{
|
||||
$filter = new GreaterThanOrEqualFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
225
tests/php/ORM/Filters/LessThanFilterTest.php
Normal file
225
tests/php/ORM/Filters/LessThanFilterTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\LessThanFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class LessThanFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'somevalue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '2',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '12',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 12,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// unicode matches - macrons are "greater" than their non-macron equivalent
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, '99', '50'],
|
||||
'matchValue' => '200',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, '250', '50'],
|
||||
'matchValue' => 200,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, bool $matches)
|
||||
{
|
||||
$filter = new LessThanFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
225
tests/php/ORM/Filters/LessThanOrEqualFilterTest.php
Normal file
225
tests/php/ORM/Filters/LessThanOrEqualFilterTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\LessThanOrEqualFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class LessThanOrEqualFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => true,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => false,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => null,
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '',
|
||||
'matchValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'SomeValue',
|
||||
'matchValue' => 'somevalue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '2',
|
||||
'matchValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 2,
|
||||
'matchValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => '1',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 1,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => '12',
|
||||
'matchValue' => 2,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 12,
|
||||
'matchValue' => '2',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// unicode matches - macrons are "greater" than their non-macron equivalent
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, '99', '50'],
|
||||
'matchValue' => '200',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, '250', '50'],
|
||||
'matchValue' => 200,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'matchValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, bool $matches)
|
||||
{
|
||||
$filter = new LessThanOrEqualFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
277
tests/php/ORM/Filters/PartialMatchFilterTest.php
Normal file
277
tests/php/ORM/Filters/PartialMatchFilterTest.php
Normal file
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\PartialMatchFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class PartialMatchFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
'null partially matches null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'null partially matches empty' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty partially matches null' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty partially matches empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'false partially matches empty' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'true doesnt partially match empty' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'empty partially matches false' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty doesnt partially match true' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false partially matches null' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'null partially matches false' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'true doesnt partially match false' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt partially match true' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false partially matches false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'true partially matches true' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'number is cast to string' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'numeric match' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'partial numeric match' => [
|
||||
'filterValue' => '1',
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'partial numeric match2' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'partial numeric match3' => [
|
||||
'filterValue' => 0,
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'case sensitive match' => [
|
||||
'filterValue' => 'SomeValue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'case sensitive mismatch' => [
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
'case sensitive partial match' => [
|
||||
'filterValue' => 'meVal',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'case sensitive partial mismatch' => [
|
||||
'filterValue' => 'meval',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
// unicode matches
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, 'somevalue', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'meVal', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'meval', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => [4, 5, 6],
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// These will both evaluate to true because the __toString() method just returns the class name.
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// case insensitive
|
||||
[
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'some',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'meval',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'different',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = $scenario['matches'] === null ? null : !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
// explicit case sensitive
|
||||
foreach ($scenarios as $scenario) {
|
||||
if (!in_array('nocase', $scenario['modifiers'])) {
|
||||
$scenario['modifiers'][] = 'case';
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $objValue, array $modifiers, ?bool $matches)
|
||||
{
|
||||
// Test with explicit default case sensitivity rather than relying on the collation, so that database
|
||||
// settings don't interfere with the test
|
||||
foreach ([true, false] as $caseSensitive) {
|
||||
// Handle cases where the expected value can depend on the default case sensitivity
|
||||
if ($matches === null) {
|
||||
$nullMatch = !(in_array('case', $modifiers) ?: $caseSensitive);
|
||||
if (in_array('not', $modifiers)) {
|
||||
$nullMatch = !$nullMatch;
|
||||
}
|
||||
}
|
||||
|
||||
PartialMatchFilter::config()->set('default_case_sensitive', $caseSensitive);
|
||||
$filter = new PartialMatchFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches ?? $nullMatch, $filter->matches($objValue));
|
||||
}
|
||||
}
|
||||
}
|
277
tests/php/ORM/Filters/StartsWithFilterTest.php
Normal file
277
tests/php/ORM/Filters/StartsWithFilterTest.php
Normal file
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Filters;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\Filters\StartsWithFilter;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class StartsWithFilterTest extends SapphireTest
|
||||
{
|
||||
|
||||
public function provideMatches()
|
||||
{
|
||||
$scenarios = [
|
||||
// without modifiers
|
||||
'null starts with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty starts with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'null starts with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty starts with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'empty starts with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'true doesnt start with empty' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => '',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt start with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt start with empty' => [
|
||||
'filterValue' => '',
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'null starts with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => null,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'false doesnt start with null' => [
|
||||
'filterValue' => null,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt start with true' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt start with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'false doesnt start with false' => [
|
||||
'filterValue' => false,
|
||||
'objValue' => false,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'true doesnt start with true' => [
|
||||
'filterValue' => true,
|
||||
'objValue' => true,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'number is cast to string' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => '1',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'1 starts with 1' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => 1,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'100 starts with 1' => [
|
||||
'filterValue' => '1',
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'100 still starts with 1' => [
|
||||
'filterValue' => 1,
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'100 doesnt start with 0' => [
|
||||
'filterValue' => 0,
|
||||
'objValue' => 100,
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'SomeValue starts with SomeValue' => [
|
||||
'filterValue' => 'SomeValue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'SomeValue doesnt start with somevalue' => [
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
'SomeValue doesnt start with meVal' => [
|
||||
'filterValue' => 'meVal',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
'SomeValue starts with Some' => [
|
||||
'filterValue' => 'Some',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
'SomeValue doesnt start with with sOmE' => [
|
||||
'filterValue' => 'sOmE',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
// unicode matches
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohuto',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohuto',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'tohutō',
|
||||
'matchValue' => 'tohutō',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// Some multi-value tests
|
||||
[
|
||||
'filterValue' => [123, 'somevalue', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => null,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'Some', 'abc'],
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => [123, 'meVal', 'abc'],
|
||||
'objValue' => 'Some',
|
||||
'modifiers' => [],
|
||||
'matches' => false,
|
||||
],
|
||||
// These will both evaluate to true because the __toString() method just returns the class name.
|
||||
// We're testing this scenario because ArrayList might contain arbitrary values
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => new ArrayData(['SomeField' => 'SoMe VaLuE']),
|
||||
'objValue' => new ArrayData(['SomeField' => 'some value']),
|
||||
'modifiers' => [],
|
||||
'matches' => true,
|
||||
],
|
||||
// case insensitive
|
||||
[
|
||||
'filterValue' => 'somevalue',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'sOmE',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => true,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'meval',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
[
|
||||
'filterValue' => 'different',
|
||||
'objValue' => 'SomeValue',
|
||||
'modifiers' => ['nocase'],
|
||||
'matches' => false,
|
||||
],
|
||||
];
|
||||
// negated
|
||||
foreach ($scenarios as $scenario) {
|
||||
$scenario['modifiers'][] = 'not';
|
||||
$scenario['matches'] = $scenario['matches'] === null ? null : !$scenario['matches'];
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
// explicit case sensitive
|
||||
foreach ($scenarios as $scenario) {
|
||||
if (!in_array('nocase', $scenario['modifiers'])) {
|
||||
$scenario['modifiers'][] = 'case';
|
||||
$scenarios[] = $scenario;
|
||||
}
|
||||
}
|
||||
return $scenarios;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatches
|
||||
*/
|
||||
public function testMatches(mixed $filterValue, mixed $matchValue, array $modifiers, ?bool $matches)
|
||||
{
|
||||
// Test with explicit default case sensitivity rather than relying on the collation, so that database
|
||||
// settings don't interfere with the test
|
||||
foreach ([true, false] as $caseSensitive) {
|
||||
// Handle cases where the expected value can depend on the default case sensitivity
|
||||
if ($matches === null) {
|
||||
$nullMatch = !(in_array('case', $modifiers) ?: $caseSensitive);
|
||||
if (in_array('not', $modifiers)) {
|
||||
$nullMatch = !$nullMatch;
|
||||
}
|
||||
}
|
||||
|
||||
StartsWithFilter::config()->set('default_case_sensitive', $caseSensitive);
|
||||
$filter = new StartsWithFilter();
|
||||
$filter->setValue($filterValue);
|
||||
$filter->setModifiers($modifiers);
|
||||
$this->assertSame($matches ?? $nullMatch, $filter->matches($matchValue));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user