-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW Handle WithinRangeFilter in searchable_fields and SearchContext #11602
Changes from all commits
842d328
d6751ae
c0880fa
d047b6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,35 +3,24 @@ | |
namespace SilverStripe\Core\Validation\FieldValidation; | ||
|
||
use SilverStripe\Core\Validation\ValidationResult; | ||
use SilverStripe\ORM\FieldType\DBInt; | ||
|
||
/** | ||
* Validates that a value is a 32-bit signed integer | ||
*/ | ||
class IntFieldValidator extends NumericNonStringFieldValidator | ||
{ | ||
/** | ||
* The minimum value for a signed 32-bit integer. | ||
* Defined as string instead of int because be cast to a float | ||
* on 32-bit systems if defined as an int | ||
*/ | ||
protected const MIN_INT = '-2147483648'; | ||
|
||
/** | ||
* The maximum value for a signed 32-bit integer. | ||
*/ | ||
protected const MAX_INT = '2147483647'; | ||
Comment on lines
-12
to
-22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are moved to |
||
|
||
public function __construct( | ||
string $name, | ||
mixed $value, | ||
?int $minValue = null, | ||
?int $maxValue = null | ||
) { | ||
if (is_null($minValue)) { | ||
$minValue = (int) static::MIN_INT; | ||
$minValue = (int) DBInt::getMinValue(); | ||
} | ||
if (is_null($maxValue)) { | ||
$maxValue = (int) static::MAX_INT; | ||
$maxValue = (int) DBInt::getMaxValue(); | ||
} | ||
parent::__construct($name, $value, $minValue, $maxValue); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
use SilverStripe\Core\Validation\ValidationResult; | ||
use SilverStripe\Dev\Debug; | ||
use SilverStripe\Dev\Deprecation; | ||
use SilverStripe\Forms\FieldGroup; | ||
use SilverStripe\Forms\FieldList; | ||
use SilverStripe\Forms\FormField; | ||
use SilverStripe\Forms\FormScaffolder; | ||
|
@@ -46,6 +47,7 @@ | |
use SilverStripe\Security\Security; | ||
use SilverStripe\View\SSViewer; | ||
use SilverStripe\Model\ModelData; | ||
use SilverStripe\ORM\Filters\WithinRangeFilter; | ||
use stdClass; | ||
|
||
/** | ||
|
@@ -2438,40 +2440,7 @@ public function scaffoldSearchFields($_params = null) | |
continue; | ||
} | ||
|
||
// If a custom fieldclass is provided as a string, use it | ||
$field = null; | ||
if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) { | ||
$fieldClass = $params['fieldClasses'][$fieldName]; | ||
$field = new $fieldClass($fieldName); | ||
// If we explicitly set a field, then construct that | ||
} elseif (isset($spec['field'])) { | ||
// If it's a string, use it as a class name and construct | ||
if (is_string($spec['field'])) { | ||
$fieldClass = $spec['field']; | ||
$field = new $fieldClass($fieldName); | ||
|
||
// If it's a FormField object, then just use that object directly. | ||
} elseif ($spec['field'] instanceof FormField) { | ||
$field = $spec['field']; | ||
|
||
// Otherwise we have a bug | ||
} else { | ||
user_error("Bad value for searchable_fields, 'field' value: " | ||
. var_export($spec['field'], true), E_USER_WARNING); | ||
} | ||
|
||
// Otherwise, use the database field's scaffolder | ||
} elseif ($object = $this->relObject($fieldName)) { | ||
if (is_object($object) && $object->hasMethod('scaffoldSearchField')) { | ||
$field = $object->scaffoldSearchField(); | ||
} else { | ||
throw new Exception(sprintf( | ||
"SearchField '%s' on '%s' does not return a valid DBField instance.", | ||
$fieldName, | ||
get_class($this) | ||
)); | ||
} | ||
} | ||
$field = $this->scaffoldSingleSearchField($fieldName, $spec, $params); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was a lot of code to parse through just to see that it's all for this singular purpose. Made sense to abstract into its own method here to encapsulate this functionality, reducing cognitive load for anyone working in this area in the future. |
||
|
||
// Allow fields to opt out of search | ||
if (!$field) { | ||
|
@@ -2483,6 +2452,24 @@ public function scaffoldSearchFields($_params = null) | |
} | ||
$field->setTitle($spec['title']); | ||
|
||
// If we're using a WithinRangeFilter, split the field into two separate fields (from and to) | ||
if (is_a($spec['filter'] ?? '', WithinRangeFilter::class, true)) { | ||
$fieldFrom = $field; | ||
$fieldTo = clone $field; | ||
$originalTitle = $field->Title(); | ||
$originalName = $field->getName(); | ||
|
||
$fieldFrom->setName($originalName . '_SearchFrom'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
$fieldFrom->setTitle(_t(__CLASS__ . '.FILTER_WITHINRANGE_FROM', 'From')); | ||
$fieldTo->setName($originalName . '_SearchTo'); | ||
$fieldTo->setTitle(_t(__CLASS__ . '.FILTER_WITHINRANGE_TO', 'To')); | ||
|
||
$field = FieldGroup::create( | ||
$originalTitle, | ||
[$fieldFrom, $fieldTo] | ||
)->setName($originalName)->addExtraClass('fieldgroup--fill-width'); | ||
} | ||
|
||
$fields->push($field); | ||
} | ||
|
||
|
@@ -2498,6 +2485,56 @@ public function scaffoldSearchFields($_params = null) | |
return $fields; | ||
} | ||
|
||
/** | ||
* Scaffold a single form field for use by the search context form. | ||
*/ | ||
private function scaffoldSingleSearchField(string $fieldName, array $spec, ?array $params): ?FormField | ||
{ | ||
// If a custom fieldclass is provided as a string, use it | ||
$field = null; | ||
if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) { | ||
$fieldClass = $params['fieldClasses'][$fieldName]; | ||
$field = new $fieldClass($fieldName); | ||
// If we explicitly set a field, then construct that | ||
} elseif (isset($spec['field'])) { | ||
// If it's a string, use it as a class name and construct | ||
if (is_string($spec['field'])) { | ||
$fieldClass = $spec['field']; | ||
$field = new $fieldClass($fieldName); | ||
|
||
// If it's a FormField object, then just use that object directly. | ||
} elseif ($spec['field'] instanceof FormField) { | ||
$field = $spec['field']; | ||
|
||
// Otherwise we have a bug | ||
} else { | ||
user_error("Bad value for searchable_fields, 'field' value: " | ||
. var_export($spec['field'], true), E_USER_WARNING); | ||
} | ||
// Use the explicitly defined dataType if one was set | ||
} elseif (isset($spec['dataType'])) { | ||
$object = Injector::inst()->get($spec['dataType'], true); | ||
$field = $this->scaffoldFieldFromObject($object, $fieldName); | ||
$field->setName($fieldName); | ||
// Otherwise, use the database field's scaffolder | ||
} elseif ($object = $this->relObject($fieldName)) { | ||
$field = $this->scaffoldFieldFromObject($object, $fieldName); | ||
} | ||
return $field; | ||
} | ||
|
||
private function scaffoldFieldFromObject(mixed $object, string $fieldName): FormField | ||
{ | ||
if (!is_object($object) || !$object->hasMethod('scaffoldSearchField')) { | ||
throw new LogicException(sprintf( | ||
"SearchField '%s' on '%s' does not return a valid DBField instance.", | ||
$fieldName, | ||
get_class($this) | ||
)); | ||
} | ||
return $object->scaffoldSearchField(); | ||
} | ||
|
||
/** | ||
* Scaffold a simple edit form for all properties on this dataobject, | ||
* based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}. | ||
|
@@ -3896,28 +3933,40 @@ public function searchableFields() | |
$rewrite = []; | ||
foreach ($fields as $name => $specOrName) { | ||
$identifier = (is_int($name)) ? $specOrName : $name; | ||
$relObject = isset($specOrName['match_any']) ? null : $this->relObject($identifier); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was only being called if the spec is an array - but that means if you're trying to search against a field that isn't valid, you only get an exception if you're using complex config! Otherwise you have to filter to get the exception, which isn't very friendly. Note that it is possible for this to return |
||
|
||
if (is_int($name)) { | ||
// Format: array('MyFieldName') | ||
$rewrite[$identifier] = []; | ||
} elseif (is_array($specOrName) && (isset($specOrName['match_any']))) { | ||
$rewrite[$identifier] = $fields[$identifier]; | ||
$rewrite[$identifier]['match_any'] = $specOrName['match_any']; | ||
} elseif (is_array($specOrName) && ($relObject = $this->relObject($identifier))) { | ||
} elseif (is_array($specOrName)) { | ||
Comment on lines
-3906
to
+3944
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The condition would cause the following problem: You have config like this: [
'MyRelation' => [
'filter' => 'ExactMatch',
],
] Because this is an array, but the relObject returns null, this condition isn't met. So we go to the [
'MyRelation' => [
'filter' => [
'filter' => 'ExactMatch',
],
],
] |
||
// Format: array('MyFieldName' => array( | ||
// 'filter => 'ExactMatchFilter', | ||
// 'field' => 'NumericField', // optional | ||
// 'title' => 'My Title', // optional | ||
// 'dataType' => DBInt::class // optional | ||
// These two are only required if using WithinRangeFilter with a data type that doesn't | ||
// inherently represent a date, time, or number | ||
// 'rangeFromDefault' => PHP_INT_MIN | ||
// 'rangeToDefault' => PHP_INT_MAX | ||
// )) | ||
$rewrite[$identifier] = array_merge( | ||
['filter' => $relObject->config()->get('default_search_filter_class')], | ||
[ | ||
'filter' => $relObject?->config()->get('default_search_filter_class'), | ||
'dataType' => $relObject ? get_class($relObject) : null, | ||
Comment on lines
+3957
to
+3958
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's no relObject, these will just default to |
||
], | ||
(array)$specOrName | ||
); | ||
} else { | ||
// Format: array('MyFieldName' => 'ExactMatchFilter') | ||
$rewrite[$identifier] = [ | ||
'filter' => $specOrName, | ||
]; | ||
if ($relObject !== null) { | ||
$rewrite[$identifier]['dataType'] ??= get_class($relObject); | ||
} | ||
} | ||
if (!isset($rewrite[$identifier]['title'])) { | ||
$rewrite[$identifier]['title'] = (isset($labels[$identifier])) | ||
|
@@ -3926,6 +3975,33 @@ public function searchableFields() | |
if (!isset($rewrite[$identifier]['filter'])) { | ||
$rewrite[$identifier]['filter'] = 'PartialMatchFilter'; | ||
} | ||
// When using a WithinRangeFilter we need to know what the default from and to values | ||
// should be, so that if a user only enters one of the two fields the other can be | ||
// populated appropriately within the filter. | ||
if (is_a($rewrite[$identifier]['filter'], WithinRangeFilter::class, true)) { | ||
// The dataType requirement here is explicitly for WithinRangeFilter. | ||
// DO NOT make it mandatory for other filters without first checking if this breaks | ||
// anything for filtering a relation, where the class on the other end of the relation | ||
// implements scaffoldSearchField(). | ||
$dataType = $rewrite[$identifier]['dataType'] ?? null; | ||
if (!is_a($dataType ?? '', DBField::class, true)) { | ||
throw new LogicException("dataType must be set to a DBField class for '$identifier'"); | ||
} | ||
if (!isset($rewrite[$identifier]['rangeFromDefault'])) { | ||
$fromDefault = $dataType::getMinValue(); | ||
if ($fromDefault === null) { | ||
throw new LogicException("rangeFromDefault must be set for '$identifier'"); | ||
} | ||
$rewrite[$identifier]['rangeFromDefault'] = $fromDefault; | ||
} | ||
if (!isset($rewrite[$identifier]['rangeToDefault'])) { | ||
$toDefault = $dataType::getMaxValue(); | ||
if ($toDefault === null) { | ||
throw new LogicException("rangeToDefault must be set for '$identifier'"); | ||
} | ||
$rewrite[$identifier]['rangeToDefault'] = $toDefault; | ||
} | ||
} | ||
} | ||
|
||
$fields = $rewrite; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,21 @@ | |
*/ | ||
class DBBigInt extends DBInt | ||
{ | ||
/** | ||
* The minimum value for a signed 64-bit integer. | ||
* Defined as string instead of int otherwise will end up as a float | ||
* on 64-bit systems | ||
* | ||
* When this is cast to an int in IntFieldValidator::__construct() | ||
* it will be properly cast to an int | ||
*/ | ||
protected const MIN_INT = '-9223372036854775808'; | ||
|
||
/** | ||
* The maximum value for a signed 64-bit integer. | ||
*/ | ||
protected const MAX_INT = '9223372036854775807'; | ||
Comment on lines
+17
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These override consts of the same name from |
||
|
||
private static array $field_validators = [ | ||
// Remove parent validator and add BigIntValidator instead | ||
IntFieldValidator::class => null, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are moved to
DBBigInt
so they can be used with the newgetMinValue()
andgetMaxValue()
.