From 4e53c35b534ab16483dba648145ac51a24e96b3e Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 30 Jun 2022 10:53:02 +1200 Subject: [PATCH 1/4] MNT Deprecate unusable property This property must always have the value 'AND' or an exception will be thrown - it should be deprecated. Also, no need to process the query before throwing. --- src/ORM/Search/SearchContext.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ORM/Search/SearchContext.php b/src/ORM/Search/SearchContext.php index 56e668411..09cee24bc 100644 --- a/src/ORM/Search/SearchContext.php +++ b/src/ORM/Search/SearchContext.php @@ -74,7 +74,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'; @@ -146,6 +147,10 @@ class SearchContext */ 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) { @@ -204,10 +209,6 @@ class SearchContext } } - if ($this->connective != "AND") { - throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite."); - } - return $query; } From 6c016615126158a9549d45faee8f6f703263a34e Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 30 Jun 2022 10:54:20 +1200 Subject: [PATCH 2/4] FIX Support search filters with match_any searchable_fields --- src/ORM/Search/SearchContext.php | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/ORM/Search/SearchContext.php b/src/ORM/Search/SearchContext.php index 09cee24bc..ed74faa1f 100644 --- a/src/ORM/Search/SearchContext.php +++ b/src/ORM/Search/SearchContext.php @@ -17,6 +17,7 @@ use SilverStripe\Forms\SelectField; use SilverStripe\Forms\CheckboxField; use InvalidArgumentException; use Exception; +use SilverStripe\ORM\DataQuery; /** * Manages searching of properties on one or more {@link DataObject} @@ -179,28 +180,25 @@ class SearchContext $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']); From 30cd521029b8d6c1f93ed1482c6ea6ee5df6aeb2 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 30 Jun 2022 12:20:17 +1200 Subject: [PATCH 3/4] DOC Update docs for searchable and summary fields. --- .../00_Model/11_Scaffolding.md | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md b/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md index 18a90bbc2..f4f768b20 100644 --- a/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md +++ b/docs/en/02_Developer_Guides/00_Model/11_Scaffolding.md @@ -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; @@ -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`: @@ -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 @@ -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', ] @@ -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. @@ -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 @@ -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 @@ -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; @@ -283,6 +312,7 @@ class MyDataObject extends DataObject ]; } ``` + ## Related Documentation * [SearchFilters](searchfilters) From ffcaed84f3a35380ac55fcbd7f3e03f1a26ba31b Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 30 Jun 2022 13:26:35 +1200 Subject: [PATCH 4/4] MNT Update tests for searchable_fields match_any --- tests/php/ORM/Search/SearchContextTest.php | 42 ++++++++++++++++++- tests/php/ORM/Search/SearchContextTest.yml | 17 ++++++++ .../ORM/Search/SearchContextTest/Order.php | 30 +++++++++++-- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/tests/php/ORM/Search/SearchContextTest.php b/tests/php/ORM/Search/SearchContextTest.php index 18010b273..c3a0a379f 100644 --- a/tests/php/ORM/Search/SearchContextTest.php +++ b/tests/php/ORM/Search/SearchContextTest.php @@ -262,10 +262,48 @@ class SearchContextTest extends SapphireTest // 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); } } diff --git a/tests/php/ORM/Search/SearchContextTest.yml b/tests/php/ORM/Search/SearchContextTest.yml index 5eb4d4022..2759311eb 100644 --- a/tests/php/ORM/Search/SearchContextTest.yml +++ b/tests/php/ORM/Search/SearchContextTest.yml @@ -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 diff --git a/tests/php/ORM/Search/SearchContextTest/Order.php b/tests/php/ORM/Search/SearchContextTest/Order.php index 6aae69b44..3df920181 100644 --- a/tests/php/ORM/Search/SearchContextTest/Order.php +++ b/tests/php/ORM/Search/SearchContextTest/Order.php @@ -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, @@ -19,12 +23,30 @@ class Order extends DataObject implements TestOnly 'CustomFirstName' => [ 'title' => 'First Name', 'field' => TextField::class, - 'filter' => 'PartialMatchFilter', 'match_any' => [ - // Searching with "First Name" will show Orders with matching Customer or Address names + // 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' => [ + 'Name', + 'Customer.FirstName', + 'ShippingAddress.FirstName', + ], + ], + 'ExactMatchField' => [ + 'field' => TextField::class, + 'filter' => 'ExactMatchFilter', + 'match_any' => [ + 'Name', + 'Customer.FirstName', + 'ShippingAddress.FirstName', + ], + ], ]; }