NEW Search across multiple searchable fields by default (#10382)

* NEW Search across multiple searchable fields by default

* ENH Split search query and search each term separately.
This commit is contained in:
Guy Sartorelli 2022-08-01 12:19:02 +12:00 committed by GitHub
parent c7504aa337
commit 11595952f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 529 additions and 38 deletions

View File

@ -148,6 +148,7 @@ en:
one: 'A Data Object'
other: '{count} Data Objects'
SINGULARNAME: 'Data Object'
GENERALSEARCH: 'General Search'
SilverStripe\ORM\FieldType\DBBoolean:
ANY: Any
NOANSWER: 'No'

View File

@ -16,6 +16,7 @@ use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\FormScaffolder;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\HiddenField;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\ORM\Connect\MySQLSchemaManager;
@ -23,6 +24,7 @@ use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBEnum;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLDelete;
use SilverStripe\ORM\Search\SearchContext;
@ -2374,6 +2376,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
);
}
/**
* Name of the field which is used as a stand-in for searching across all searchable fields.
*
* If this is a blank string, general search functionality is disabled
* and the general search field falls back to using the first field in
* the searchable fields array.
*/
public function getGeneralSearchFieldName(): string
{
return $this->config()->get('general_search_field_name');
}
/**
* Determine which properties on the DataObject are
* searchable, and map them to their default {@link FormField}
@ -2399,6 +2413,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
(array)$_params
);
$fields = new FieldList();
foreach ($this->searchableFields() as $fieldName => $spec) {
if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'] ?? [])) {
continue;
@ -2451,6 +2466,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$fields->push($field);
}
// Only include general search if there are fields it can search on
$generalSearch = $this->getGeneralSearchFieldName();
if ($generalSearch !== '' && $fields->count() > 0) {
if ($fields->fieldByName($generalSearch) || $fields->dataFieldByName($generalSearch)) {
throw new LogicException('General search field name must be unique.');
}
$fields->unshift(HiddenField::create($generalSearch, _t(self::class . 'GENERALSEARCH', 'General Search')));
}
return $fields;
}
@ -4198,6 +4223,29 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
private static $searchable_fields = null;
/**
* Name of the field which is used as a stand-in for searching across all searchable fields.
*
* If this is a blank string, general search functionality is disabled
* and the general search field falls back to using the first field in
* the searchable_fields array.
*/
private static string $general_search_field_name = 'q';
/**
* The search filter to use when searching with the general search field.
* If this is an empty string, the search filters configured for each field are used instead.
*/
private static string $general_search_field_filter = PartialMatchFilter::class;
/**
* If true, the search phrase is split into individual terms, and checks all searchable fields for each search term.
* If false, all fields are checked for the entire search phrase as a whole.
*
* Note that splitting terms may cause unexpected resuls if using an ExactMatchFilter.
*/
private static bool $general_search_split_terms = true;
/**
* User defined labels for searchable_fields, used to override
* default display in the search form.

View File

@ -59,7 +59,7 @@ class PartialMatchFilter extends SearchFilter
);
$clause = [$comparisonClause => $this->getMatchPattern($this->getValue())];
return $this->aggregate ?
$this->applyAggregate($query, $clause) :
$query->where($clause);

View File

@ -17,6 +17,7 @@ use SilverStripe\Forms\SelectField;
use SilverStripe\Forms\CheckboxField;
use InvalidArgumentException;
use Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataQuery;
/**
@ -110,8 +111,6 @@ class SearchContext
public function getSearchFields()
{
return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
// $this->fields is causing weirdness, so we ignore for now, using the default scaffolding
//return singleton($this->modelClass)->scaffoldSearchFields();
}
/**
@ -151,8 +150,38 @@ class SearchContext
if ($this->connective != "AND") {
throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
}
$this->setSearchParams($searchParams);
$query = $this->prepareQuery($sort, $limit, $existingQuery);
return $this->search($query);
}
/** DataList $query */
/**
* Perform a search on the passed DataList based on $this->searchParams.
*/
private function search(DataList $query): DataList
{
/** @var DataObject $modelObj */
$modelObj = Injector::inst()->create($this->modelClass);
$searchableFields = $modelObj->searchableFields();
foreach ($this->searchParams as $searchField => $searchPhrase) {
$searchField = str_replace('__', '.', $searchField ?? '');
if ($searchField !== '' && $searchField === $modelObj->getGeneralSearchFieldName()) {
$query = $this->generalFieldSearch($query, $searchableFields, $searchPhrase);
} else {
$query = $this->individualFieldSearch($query, $searchableFields, $searchField, $searchPhrase);
}
}
return $query;
}
/**
* Prepare the query to begin searching
*
* @param array|bool|string $sort Database column to sort on.
* @param array|bool|string $limit
*/
private function prepareQuery($sort, $limit, ?DataList $existingQuery): DataList
{
$query = null;
if ($existingQuery) {
if (!($existingQuery instanceof DataList)) {
@ -176,38 +205,120 @@ class SearchContext
$query = $query->limit($limit);
}
/** @var DataList $query */
$query = $query->sort($sort);
$this->setSearchParams($searchParams);
return $query->sort($sort);
}
$modelObj = Injector::inst()->create($this->modelClass);
$searchableFields = $modelObj->searchableFields();
foreach ($this->searchParams as $key => $value) {
$key = str_replace('__', '.', $key ?? '');
if ($filter = $this->getFilter($key)) {
/**
* Takes a search phrase or search term and searches for it across all searchable fields.
*
* @param string|array $searchPhrase
*/
private function generalSearchAcrossFields($searchPhrase, DataQuery $subGroup, array $searchableFields): void
{
$formFields = $this->getSearchFields();
foreach ($searchableFields as $field => $spec) {
$formFieldName = str_replace('.', '__', $field);
$filter = $this->getGeneralSearchFilter($this->modelClass, $field);
// Only apply filter if the field is allowed to be general and is backed by a form field.
// Otherwise we could be dealing with, for example, a DataObject which implements scaffoldSearchField
// to provide some unexpected field name, where the below would result in a DatabaseException.
if ((!isset($spec['general']) || $spec['general'])
&& ($formFields->fieldByName($formFieldName) || $formFields->dataFieldByName($formFieldName))
&& $filter !== null
) {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if (!$filter->isEmpty()) {
if (isset($searchableFields[$key]['match_any'])) {
$searchFields = $searchableFields[$key]['match_any'];
$filterClass = get_class($filter);
$modifiers = $filter->getModifiers();
$query = $query->alterDataQuery(function (DataQuery $dataQuery) use ($searchFields, $filterClass, $modifiers, $value) {
$subGroup = $dataQuery->disjunctiveGroup();
foreach ($searchFields as $matchField) {
/** @var SearchFilter $filterClass */
$filter = new $filterClass($matchField, $value, $modifiers);
$filter->apply($subGroup);
}
});
} else {
$query = $query->alterDataQuery([$filter, 'apply']);
}
}
$filter->setValue($searchPhrase);
$this->applyFilter($filter, $subGroup, $spec);
}
}
}
return $query;
/**
* Use the global general search for searching across multiple fields.
*
* @param string|array $searchPhrase
*/
private function generalFieldSearch(DataList $query, array $searchableFields, $searchPhrase): DataList
{
return $query->alterDataQuery(function (DataQuery $dataQuery) use ($searchableFields, $searchPhrase) {
// If necessary, split search phrase into terms, then search across fields.
if (Config::inst()->get($this->modelClass, 'general_search_split_terms')) {
if (is_array($searchPhrase)) {
// Allow matches from ANY query in the array (i.e. return $obj where query1 matches OR query2 matches)
$dataQuery = $dataQuery->disjunctiveGroup();
foreach ($searchPhrase as $phrase) {
// where ((field1 LIKE %lorem% OR field2 LIKE %lorem%) AND (field1 LIKE %ipsum% OR field2 LIKE %ipsum%))
$generalSubGroup = $dataQuery->conjunctiveGroup();
foreach (explode(' ', $phrase) as $searchTerm) {
$this->generalSearchAcrossFields($searchTerm, $generalSubGroup->disjunctiveGroup(), $searchableFields);
}
}
} else {
// where ((field1 LIKE %lorem% OR field2 LIKE %lorem%) AND (field1 LIKE %ipsum% OR field2 LIKE %ipsum%))
$generalSubGroup = $dataQuery->conjunctiveGroup();
foreach (explode(' ', $searchPhrase) as $searchTerm) {
$this->generalSearchAcrossFields($searchTerm, $generalSubGroup->disjunctiveGroup(), $searchableFields);
}
}
} else {
// where (field1 LIKE %lorem ipsum% OR field2 LIKE %lorem ipsum%)
$this->generalSearchAcrossFields($searchPhrase, $dataQuery->disjunctiveGroup(), $searchableFields);
}
});
}
/**
* Get the search filter for the given fieldname when searched from the general search field.
*/
private function getGeneralSearchFilter(string $modelClass, string $fieldName): ?SearchFilter
{
if ($filterClass = Config::inst()->get($modelClass, 'general_search_field_filter')) {
return Injector::inst()->create($filterClass, $fieldName);
}
return $this->getFilter($fieldName);
}
/**
* Search against a single field
*
* @param string|array $searchPhrase
*/
private function individualFieldSearch(DataList $query, array $searchableFields, string $searchField, $searchPhrase): DataList
{
$filter = $this->getFilter($searchField);
if (!$filter) {
return $query;
}
$filter->setModel($this->modelClass);
$filter->setValue($searchPhrase);
$searchableFieldSpec = $searchableFields[$searchField] ?? [];
return $query->alterDataQuery(function ($dataQuery) use ($filter, $searchableFieldSpec) {
$this->applyFilter($filter, $dataQuery, $searchableFieldSpec);
});
}
/**
* Apply a SearchFilter to a DataQuery for a given field's specifications
*/
private function applyFilter(SearchFilter $filter, DataQuery $dataQuery, array $searchableFieldSpec): void
{
if ($filter->isEmpty()) {
return;
}
if (isset($searchableFieldSpec['match_any'])) {
$searchFields = $searchableFieldSpec['match_any'];
$filterClass = get_class($filter);
$value = $filter->getValue();
$modifiers = $filter->getModifiers();
$subGroup = $dataQuery->disjunctiveGroup();
foreach ($searchFields as $matchField) {
/** @var SearchFilter $filter */
$filter = Injector::inst()->create($filterClass, $matchField, $value, $modifiers);
$filter->apply($subGroup);
}
} else {
$filter->apply($dataQuery);
}
}
/**

View File

@ -14,8 +14,8 @@ use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\CheerleaderHat;
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\Mom;
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\Team;
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\TeamGroup;
use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\TestController;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
class GridFieldFilterHeaderTest extends SapphireTest
{
@ -89,9 +89,12 @@ class GridFieldFilterHeaderTest extends SapphireTest
public function testSearchFieldSchema()
{
$searchSchema = json_decode($this->component->getSearchFieldSchema($this->gridField) ?? '');
$modelClass = $this->gridField->getModelClass();
/** @var DataObject $obj */
$obj = new $modelClass();
$this->assertEquals('field/testfield/schema/SearchForm', $searchSchema->formSchemaUrl);
$this->assertEquals('Name', $searchSchema->name);
$this->assertEquals($obj->getGeneralSearchFieldName(), $searchSchema->name);
$this->assertEquals('Search "Teams"', $searchSchema->placeholder);
$this->assertEquals(new \stdClass, $searchSchema->filters);
@ -110,9 +113,12 @@ class GridFieldFilterHeaderTest extends SapphireTest
);
$this->gridField->setRequest($request);
$searchSchema = json_decode($this->component->getSearchFieldSchema($this->gridField) ?? '');
$modelClass = $this->gridField->getModelClass();
/** @var DataObject $obj */
$obj = new $modelClass();
$this->assertEquals('field/testfield/schema/SearchForm', $searchSchema->formSchemaUrl);
$this->assertEquals('Name', $searchSchema->name);
$this->assertEquals($obj->getGeneralSearchFieldName(), $searchSchema->name);
$this->assertEquals('Search "Teams"', $searchSchema->placeholder);
$this->assertEquals('test', $searchSchema->filters->Search__Name);
$this->assertEquals('place', $searchSchema->filters->Search__City);
@ -145,9 +151,10 @@ class GridFieldFilterHeaderTest extends SapphireTest
$searchForm = $this->component->getSearchForm($this->gridField);
$this->assertTrue($searchForm instanceof Form);
$this->assertEquals('Search__Name', $searchForm->fields[0]->Name);
$this->assertEquals('Search__City', $searchForm->fields[1]->Name);
$this->assertEquals('Search__Cheerleader__Hat__Colour', $searchForm->fields[2]->Name);
$this->assertEquals('Search__q', $searchForm->fields[0]->Name);
$this->assertEquals('Search__Name', $searchForm->fields[1]->Name);
$this->assertEquals('Search__City', $searchForm->fields[2]->Name);
$this->assertEquals('Search__Cheerleader__Hat__Colour', $searchForm->fields[3]->Name);
$this->assertEquals('TeamsSearchForm', $searchForm->Name);
$this->assertEquals('cms-search-form', $searchForm->extraClasses['cms-search-form']);

View File

@ -2,6 +2,9 @@
namespace SilverStripe\ORM\Tests\Search;
use LogicException;
use ReflectionMethod;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\TextareaField;
@ -9,7 +12,10 @@ use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HiddenField;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\Filters\EndsWithFilter;
use SilverStripe\ORM\Filters\ExactMatchFilter;
use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Search\SearchContext;
@ -20,6 +26,7 @@ class SearchContextTest extends SapphireTest
protected static $extra_dataobjects = [
SearchContextTest\Person::class,
SearchContextTest\NoSearchableFields::class,
SearchContextTest\Book::class,
SearchContextTest\Company::class,
SearchContextTest\Project::class,
@ -29,6 +36,7 @@ class SearchContextTest extends SapphireTest
SearchContextTest\Customer::class,
SearchContextTest\Address::class,
SearchContextTest\Order::class,
SearchContextTest\GeneralSearch::class,
];
public function testResultSetFilterReturnsExpectedCount()
@ -45,6 +53,20 @@ class SearchContextTest extends SapphireTest
$this->assertEquals(1, $results->Count());
}
public function testSearchableFieldsDefaultsToSummaryFields()
{
$obj = new SearchContextTest\NoSearchableFields();
$summaryFields = $obj->summaryFields();
$expected = [];
foreach ($summaryFields as $field => $label) {
$expected[$field] = [
'title' => $obj->fieldLabel($field),
'filter' => 'PartialMatchFilter',
];
}
$this->assertEquals($expected, $obj->searchableFields());
}
public function testSummaryIncludesDefaultFieldsIfNotDefined()
{
$person = SearchContextTest\Person::singleton();
@ -109,6 +131,7 @@ class SearchContextTest extends SapphireTest
$context = $company->getDefaultSearchContext();
$this->assertEquals(
new FieldList(
new HiddenField($company->getGeneralSearchFieldName(), 'General Search'),
(new TextField("Name", 'Name'))
->setMaxLength(255),
new TextareaField("Industry", 'Industry'),
@ -255,6 +278,195 @@ class SearchContextTest extends SapphireTest
$this->assertNull($nothing);
}
public function testGeneralSearch()
{
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
// Matches on a variety of fields
$results = $context->getResults([$generalField => 'General']);
$this->assertCount(2, $results);
$this->assertNotContains('MatchNothing', $results->column('Name'));
$results = $context->getResults([$generalField => 'brown']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
// Uses its own filter (not field filters)
$results = $context->getResults([$generalField => 'exact']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
// Uses match_any fields
$results = $context->getResults([$generalField => 'first']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
// Even across a relation
$results = $context->getResults([$generalField => 'arbitrary']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
}
public function testSpecificSearchFields()
{
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
$results = $context->getResults([$generalField => $general1->ExcludeThisField]);
$this->assertNotEmpty($general1->ExcludeThisField);
$this->assertCount(0, $results);
}
public function testGeneralOnlyUsesSearchableFields()
{
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
$results = $context->getResults([$generalField => $general1->DoNotUseThisField]);
$this->assertNotEmpty($general1->DoNotUseThisField);
$this->assertCount(0, $results);
}
public function testGeneralSearchSplitTerms()
{
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
// These terms don't exist in a single field in this order on any object, but they do exist in separate fields.
$results = $context->getResults([$generalField => 'general blue']);
$this->assertCount(1, $results);
$this->assertEquals('General Zero', $results->first()->Name);
// These terms exist in a single field, but not in this order.
$results = $context->getResults([$generalField => 'matches partial']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
}
public function testGeneralSearchNoSplitTerms()
{
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_split_terms', false);
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
// These terms don't exist in a single field in this order on any object
$results = $context->getResults([$generalField => 'general blue']);
$this->assertCount(0, $results);
// These terms exist in a single field, but not in this order.
$results = $context->getResults([$generalField => 'matches partial']);
$this->assertCount(0, $results);
// These terms exist in a single field in this order.
$results = $context->getResults([$generalField => 'partial matches']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
}
public function testGetGeneralSearchFilter()
{
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$getSearchFilterReflection = new ReflectionMethod($context, 'getGeneralSearchFilter');
$getSearchFilterReflection->setAccessible(true);
// By default, uses the PartialMatchFilter.
$this->assertSame(
PartialMatchFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'ExactMatchField'))
);
$this->assertSame(
PartialMatchFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'PartialMatchField'))
);
// Changing the config changes the filter.
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_filter', EndsWithFilter::class);
$this->assertSame(
EndsWithFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'ExactMatchField'))
);
$this->assertSame(
EndsWithFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'PartialMatchField'))
);
// Removing the filter config defaults to use the field's filter.
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_filter', '');
$this->assertSame(
ExactMatchFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'ExactMatchField'))
);
$this->assertSame(
PartialMatchFilter::class,
get_class($getSearchFilterReflection->invoke($context, $general1->ClassName, 'PartialMatchField'))
);
}
public function testGeneralSearchFilterIsUsed()
{
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_filter', '');
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
// Respects ExactMatchFilter
$results = $context->getResults([$generalField => 'exact']);
$this->assertCount(0, $results);
// No match when splitting terms
$results = $context->getResults([$generalField => 'This requires an exact match']);
$this->assertCount(0, $results);
// When not splitting terms, the behaviour of `ExactMatchFilter` is slightly different.
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_split_terms', false);
// Respects ExactMatchFilter
$results = $context->getResults([$generalField => 'exact']);
$this->assertCount(0, $results);
$results = $context->getResults([$generalField => 'This requires an exact match']);
$this->assertCount(1, $results);
$this->assertEquals('General One', $results->first()->Name);
}
public function testGeneralSearchDisabled()
{
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_name', '');
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$context = $general1->getDefaultSearchContext();
$generalField = $general1->getGeneralSearchFieldName();
$this->assertEmpty($generalField);
// Defaults to returning all objects, because the field doesn't exist in the SearchContext
$numObjs = SearchContextTest\GeneralSearch::get()->count();
$results = $context->getResults([$generalField => 'General']);
$this->assertCount($numObjs, $results);
$results = $context->getResults([$generalField => 'brown']);
$this->assertCount($numObjs, $results);
// Searching on other fields still works as expected (e.g. first field, which is the UI default in this situation)
$results = $context->getResults(['Name' => 'General']);
$this->assertCount(2, $results);
$this->assertNotContains('MatchNothing', $results->column('Name'));
}
public function testGeneralSearchCustomFieldName()
{
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_name', 'some_arbitrary_field_name');
$obj = new SearchContextTest\GeneralSearch();
$this->assertSame('some_arbitrary_field_name', $obj->getGeneralSearchFieldName());
$this->testGeneralSearch();
}
public function testGeneralSearchFieldNameMustBeUnique()
{
Config::modify()->set(SearchContextTest\GeneralSearch::class, 'general_search_field_name', 'MatchAny');
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
$this->expectException(LogicException::class);
$general1->getDefaultSearchContext();
}
public function testMatchAnySearch()
{
$order1 = $this->objFromFixture(SearchContextTest\Order::class, 'order1');

View File

@ -74,6 +74,7 @@ SilverStripe\ORM\Tests\Search\SearchContextTest\AllFilterTypes:
SilverStripe\ORM\Tests\Search\SearchContextTest\Customer:
customer1:
FirstName: Bill
MatchAny: Some arbitrary Value
customer2:
FirstName: Bailey
customer3:
@ -100,3 +101,34 @@ SilverStripe\ORM\Tests\Search\SearchContextTest\Order:
Name: 'Jack'
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer3
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address3
SilverStripe\ORM\Tests\Search\SearchContextTest\GeneralSearch:
general0:
Name: General Zero
DoNotUseThisField: omitted
HairColor: blue
ExcludeThisField: excluded
ExactMatchField: Some specific value here
PartialMatchField: A partial match is allowed for this field
MatchAny1: Some match any field
MatchAny2: Another match any field
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer2
general1:
Name: General One
DoNotUseThisField: omitted
HairColor: brown
ExcludeThisField: excluded
ExactMatchField: This requires an exact match
PartialMatchField: This explicitly allows partial matches
MatchAny1: first match
MatchAny2: second match
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer1
general2:
Name: MatchNothing
DoNotUseThisField: MatchNothing
HairColor: MatchNothing
ExcludeThisField: MatchNothing
ExactMatchField: MatchNothing
PartialMatchField: MatchNothing
MatchAny1: MatchNothing
MatchAny2: MatchNothing

View File

@ -10,6 +10,7 @@ class Customer extends DataObject implements TestOnly
private static $table_name = 'SearchContextTest_Customer';
private static $db = [
'FirstName' => 'Text'
'FirstName' => 'Text',
'MatchAny' => 'Varchar',
];
}

View File

@ -0,0 +1,50 @@
<?php
namespace SilverStripe\ORM\Tests\Search\SearchContextTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObject;
class GeneralSearch extends DataObject implements TestOnly
{
private static $table_name = 'SearchContextTest_GeneralSearch';
private static $db = [
'Name' => 'Varchar',
'DoNotUseThisField' => 'Varchar',
'HairColor' => 'Varchar',
'ExcludeThisField' => 'Varchar',
'ExactMatchField' => 'Varchar',
'PartialMatchField' => 'Varchar',
'MatchAny1' => 'Varchar',
'MatchAny2' => 'Varchar',
];
private static $has_one = [
'Customer' => Customer::class,
'ShippingAddress' => Address::class,
];
private static $searchable_fields = [
'Name',
'HairColor',
'ExcludeThisField' => [
'general' => false,
],
'ExactMatchField' => [
'filter' => 'ExactMatchFilter',
],
'PartialMatchField' => [
'filter' => 'PartialMatchFilter',
],
'MatchAny' => [
'field' => TextField::class,
'match_any' => [
'MatchAny1',
'MatchAny2',
'Customer.MatchAny',
]
]
];
}

View File

@ -0,0 +1,29 @@
<?php
namespace SilverStripe\ORM\Tests\Search\SearchContextTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class NoSearchableFields extends DataObject implements TestOnly
{
private static $table_name = 'SearchContextTest_NoSearchableFields';
private static $db = [
'Name' => 'Varchar',
'Email' => 'Varchar',
'HairColor' => 'Varchar',
'EyeColor' => 'Varchar'
];
private static $has_one = [
'Customer' => Customer::class,
];
private static $summary_fields = [
'Name' => 'Custom Label',
'Customer.FirstName' => 'Customer',
'HairColor',
'EyeColor',
];
}