Skip to content
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

FIX Support search filters with match_any searchable_fields #10380

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,20 @@ public function getCMSFields()
You can also alter the fields of built-in and module `DataObject` classes through your own
[DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`.

[info]
`FormField` scaffolding takes [`$field_labels` config](#field-labels) into account as well.
[/info]

## Searchable Fields

The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin
system. The default is a set of array values listing the fields.

[info]
`$searchable_fields` will default to use the [`$summary_fields` config](#summary-fields) if not defined. This works fine unless
your `$summary_fields` config specifies fields that are not stored in the database.
[/info]

```php
use SilverStripe\ORM\DataObject;

Expand All @@ -79,6 +88,8 @@ class MyDataObject extends DataObject
}
```

### Specify a form field or search filter

Searchable fields will appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a
default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter)). To override these defaults, you can specify
additional information on `$searchable_fields`:
Expand Down Expand Up @@ -119,6 +130,8 @@ class MyDataObject extends DataObject
}
```

### Searching on relations

To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.

```php
Expand Down Expand Up @@ -154,23 +167,29 @@ class Player extends DataObject

```

Use a single search field that matches on multiple database fields with `'match_any'`
### Searching many db fields on a single search field

Use a single search field that matches on multiple database fields with `'match_any'`. This also supports specifying a field and a filter, though it is not necessary to do so.

```php
class Order extends DataObject
{
private static $db = [
'Name' => 'Varchar',
];

private static $has_one = [
'Customer' => Customer::class,
'ShippingAddress' => Address::class,
];

private static $searchable_fields = [
'CustomFirstName' => [
'CustomName' => [
'title' => 'First Name',
'field' => TextField::class,
'filter' => 'PartialMatchFilter',
'match_any' => [
// Searching with the "First Name" field will show Orders matching either Customer.FirstName or ShippingAddress.FirstName
// Searching with the "First Name" field will show Orders matching either Name, Customer.FirstName, or ShippingAddress.FirstName
'Name',
'Customer.FirstName',
'ShippingAddress.FirstName',
]
Expand All @@ -179,7 +198,11 @@ class Order extends DataObject
}
```

### Summary Fields
[alert]
If you don't specify a field, you must use the name of a real database field instead of a custom name so that a default field can be determined.
[/alert]

## Summary Fields

Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use
is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface.
Expand All @@ -202,6 +225,8 @@ class MyDataObject extends DataObject
}
```

### Relations in summary fields

To include relations or field manipulations in your summaries, you can use a dot-notation.

```php
Expand Down Expand Up @@ -234,6 +259,8 @@ class MyDataObject extends DataObject

```

### Images in summary fields

Non-textual elements (such as images and their manipulations) can also be used in summaries.

```php
Expand All @@ -257,7 +284,9 @@ class MyDataObject extends DataObject

```

In order to re-label any summary fields, you can use the `$field_labels` static.
## Field labels

In order to re-label any summary fields, you can use the `$field_labels` static. This will also affect the output of `$object->fieldLabels()` and `$object->fieldLabel()`.

```php
use SilverStripe\ORM\DataObject;
Expand All @@ -283,6 +312,7 @@ class MyDataObject extends DataObject
];
}
```

## Related Documentation

* [SearchFilters](searchfilters)
Expand Down
39 changes: 19 additions & 20 deletions src/ORM/Search/SearchContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use SilverStripe\Forms\CheckboxField;
use InvalidArgumentException;
use Exception;
use SilverStripe\ORM\DataQuery;

/**
* Manages searching of properties on one or more {@link DataObject}
Expand Down Expand Up @@ -74,7 +75,8 @@ class SearchContext
protected $searchParams = [];

/**
* The logical connective used to join WHERE clauses. Defaults to AND.
* The logical connective used to join WHERE clauses. Must be "AND".
* @deprecated 5.0
* @var string
*/
public $connective = 'AND';
Expand Down Expand Up @@ -146,6 +148,10 @@ protected function applyBaseTableFields()
*/
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null)
{
if ($this->connective != "AND") {
throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
}

/** DataList $query */
$query = null;
if ($existingQuery) {
Expand Down Expand Up @@ -174,28 +180,25 @@ public function getQuery($searchParams, $sort = false, $limit = false, $existing
$query = $query->sort($sort);
$this->setSearchParams($searchParams);

$modelObj = Injector::inst()->create($this->modelClass);
$searchableFields = $modelObj->searchableFields();
foreach ($this->searchParams as $key => $value) {
$key = str_replace('__', '.', $key ?? '');
if ($filter = $this->getFilter($key)) {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if (!$filter->isEmpty()) {
$modelObj = Injector::inst()->create($this->modelClass);
if (isset($modelObj->searchableFields()[$key]['match_any'])) {
$query = $query->alterDataQuery(function ($dataQuery) use ($modelObj, $key, $value) {
$searchFields = $modelObj->searchableFields()[$key]['match_any'];
$sqlSearchFields = [];
foreach ($searchFields as $dottedRelation) {
$relation = substr($dottedRelation ?? '', 0, strpos($dottedRelation ?? '', '.'));
$relations = explode('.', $dottedRelation ?? '');
$fieldName = array_pop($relations);
$relationModelName = $dataQuery->applyRelation($relation);
$relationPrefix = $dataQuery->applyRelationPrefix($relation);
$columnName = $modelObj->getSchema()
->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
$sqlSearchFields[$columnName] = $value;
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);
}
$dataQuery = $dataQuery->whereAny($sqlSearchFields);
});
} else {
$query = $query->alterDataQuery([$filter, 'apply']);
Expand All @@ -204,10 +207,6 @@ public function getQuery($searchParams, $sort = false, $limit = false, $existing
}
}

if ($this->connective != "AND") {
throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
}

return $query;
}

Expand Down
42 changes: 40 additions & 2 deletions tests/php/ORM/Search/SearchContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,48 @@ public function testMatchAnySearch()

// Search should match Order's customer FirstName
$results = $context->getResults(['CustomFirstName' => 'Bill']);
$this->assertEquals(1, $results->Count());
$this->assertCount(2, $results);
$this->assertListContains([
['Name' => 'Jane'],
['Name' => 'Jack'],
], $results);

// Search should match Order's shipping address FirstName
$results = $context->getResults(['CustomFirstName' => 'Bob']);
$this->assertEquals(1, $results->Count());
$this->assertCount(2, $results);
$this->assertListContains([
['Name' => 'Jane'],
['Name' => 'Jill'],
], $results);

// Search should match Order's Name db field
$results = $context->getResults(['CustomFirstName' => 'Jane']);
$this->assertCount(1, $results);
$this->assertSame('Jane', $results->first()->Name);

// Search should not match any Order
$results = $context->getResults(['CustomFirstName' => 'NoMatches']);
$this->assertCount(0, $results);
}

public function testMatchAnySearchWithFilters()
{
$order1 = $this->objFromFixture(SearchContextTest\Order::class, 'order1');
$context = $order1->getDefaultSearchContext();

$results = $context->getResults(['ExactMatchField' => 'Bil']);
$this->assertCount(0, $results);
$results = $context->getResults(['PartialMatchField' => 'Bil']);
$this->assertCount(2, $results);

$results = $context->getResults(['ExactMatchField' => 'ob']);
$this->assertCount(0, $results);
$results = $context->getResults(['PartialMatchField' => 'ob']);
$this->assertCount(2, $results);

$results = $context->getResults(['ExactMatchField' => 'an']);
$this->assertCount(0, $results);
$results = $context->getResults(['PartialMatchField' => 'an']);
$this->assertCount(1, $results);
}
}
17 changes: 17 additions & 0 deletions tests/php/ORM/Search/SearchContextTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,29 @@ SilverStripe\ORM\Tests\Search\SearchContextTest\AllFilterTypes:
SilverStripe\ORM\Tests\Search\SearchContextTest\Customer:
customer1:
FirstName: Bill
customer2:
FirstName: Bailey
customer3:
FirstName: Billy

SilverStripe\ORM\Tests\Search\SearchContextTest\Address:
address1:
FirstName: Bob
address2:
FirstName: Barley
address3:
FirstName: Billy

SilverStripe\ORM\Tests\Search\SearchContextTest\Order:
order1:
Name: 'Jane'
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer1
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address1
order2:
Name: 'Jill'
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer2
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address1
order3:
Name: 'Jack'
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer3
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address3
28 changes: 25 additions & 3 deletions tests/php/ORM/Search/SearchContextTest/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class Order extends DataObject implements TestOnly
{
private static $table_name = 'SearchContextTest_Order';

private static $db = [
'Name' => 'Varchar',
];

private static $has_one = [
'Customer' => Customer::class,
'ShippingAddress' => Address::class,
Expand All @@ -18,13 +22,31 @@ class Order extends DataObject implements TestOnly
private static $searchable_fields = [
'CustomFirstName' => [
'title' => 'First Name',
'field' => TextField::class,
'match_any' => [
// Searching with the "First Name" field will show Orders matching either Name, Customer.FirstName, or ShippingAddress.FirstName
'Name',
'Customer.FirstName',
'ShippingAddress.FirstName',
],
],
'PartialMatchField' => [
'field' => TextField::class,
'filter' => 'PartialMatchFilter',
'match_any' => [
// Searching with "First Name" will show Orders with matching Customer or Address names
'Name',
'Customer.FirstName',
'ShippingAddress.FirstName',
],
],
'ExactMatchField' => [
'field' => TextField::class,
'filter' => 'ExactMatchFilter',
'match_any' => [
'Name',
'Customer.FirstName',
'ShippingAddress.FirstName',
]
]
],
],
];
}