diff --git a/src/ORM/Search/SearchContext.php b/src/ORM/Search/SearchContext.php index 050c72079..456868099 100644 --- a/src/ORM/Search/SearchContext.php +++ b/src/ORM/Search/SearchContext.php @@ -10,30 +10,34 @@ use SilverStripe\Forms\FormField; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataList; use SilverStripe\ORM\Filters\SearchFilter; +use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ArrayData; +use SilverStripe\Forms\SelectField; +use SilverStripe\Forms\CheckboxField; use InvalidArgumentException; use Exception; /** -* Manages searching of properties on one or more {@link DataObject} -* types, based on a given set of input parameters. -* SearchContext is intentionally decoupled from any controller-logic, -* it just receives a set of search parameters and an object class it acts on. -* -* The default output of a SearchContext is either a {@link SQLSelect} object -* for further refinement, or a {@link SS_List} that can be used to display -* search results, e.g. in a {@link TableListField} instance. -* -* In case you need multiple contexts, consider namespacing your request parameters -* by using {@link FieldList->namespace()} on the $fields constructor parameter. -* -* Each DataObject subclass can have multiple search contexts for different cases, -* e.g. for a limited frontend search and a fully featured backend search. -* By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically -* scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields -* to include. -* -* @see http://doc.silverstripe.com/doku.php?id=searchcontext -*/ + * Manages searching of properties on one or more {@link DataObject} + * types, based on a given set of input parameters. + * SearchContext is intentionally decoupled from any controller-logic, + * it just receives a set of search parameters and an object class it acts on. + * + * The default output of a SearchContext is either a {@link SQLSelect} object + * for further refinement, or a {@link SS_List} that can be used to display + * search results, e.g. in a {@link TableListField} instance. + * + * In case you need multiple contexts, consider namespacing your request parameters + * by using {@link FieldList->namespace()} on the $fields constructor parameter. + * + * Each DataObject subclass can have multiple search contexts for different cases, + * e.g. for a limited frontend search and a fully featured backend search. + * By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically + * scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields + * to include. + * + * @see http://doc.silverstripe.com/doku.php?id=searchcontext + */ class SearchContext { use Injectable; @@ -61,6 +65,13 @@ class SearchContext */ protected $filters; + /** + * Key/value pairs of search fields to search terms + * + * @var array + */ + protected $searchParams = []; + /** * The logical connective used to join WHERE clauses. Defaults to AND. * @var string @@ -110,10 +121,10 @@ class SearchContext $baseTable = DataObject::getSchema()->baseDataTable($this->modelClass); $fields = array("\"{$baseTable}\".*"); if ($this->modelClass != $classes[0]) { - $fields[] = '"'.$classes[0].'".*'; + $fields[] = '"' . $classes[0] . '".*'; } //$fields = array_keys($model->db()); - $fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"'; + $fields[] = '"' . $classes[0] . '".\"ClassName\" AS "RecordClassName"'; return $fields; } @@ -121,7 +132,7 @@ class SearchContext * Returns a SQL object representing the search context for the given * list of query parameters. * - * @param array $searchParams Map of search criteria, mostly taked from $_REQUEST. + * @param array $searchParams Map of search criteria, mostly taken from $_REQUEST. * If a filter is applied to a relationship in dot notation, * the parameter name should have the dots replaced with double underscores, * for example "Comments__Name" instead of the filter name "Comments.Name". @@ -160,20 +171,14 @@ class SearchContext /** @var DataList $query */ $query = $query->sort($sort); + $this->setSearchParams($searchParams); - // hack to work with $searchParems when it's an Object - if ($searchParams instanceof HTTPRequest) { - $searchParamArray = $searchParams->getVars(); - } else { - $searchParamArray = $searchParams; - } - - foreach ($searchParamArray as $key => $value) { + 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()) { + if (!$filter->isEmpty()) { $query = $query->alterDataQuery(array($filter, 'apply')); } } @@ -199,7 +204,7 @@ class SearchContext */ public function getResults($searchParams, $sort = false, $limit = false) { - $searchParams = array_filter((array)$searchParams, array($this,'clearEmptySearchFields')); + $searchParams = array_filter((array)$searchParams, array($this, 'clearEmptySearchFields')); // getQuery actually returns a DataList return $this->getQuery($searchParams, $sort, $limit); @@ -311,4 +316,72 @@ class SearchContext { $this->fields->removeByName($fieldName); } + + /** + * @param $searchParams + */ + public function setSearchParams($searchParams) + { + // hack to work with $searchParams when it's an Object + if ($searchParams instanceof HTTPRequest) { + $this->searchParams = $searchParams->getVars(); + } else { + $this->searchParams = $searchParams; + } + } + + /** + * @return array + */ + public function getSearchParams() + { + return $this->searchParams; + } + + /** + * Gets a list of what fields were searched and the values provided + * for each field. Returns an ArrayList of ArrayData, suitable for + * rendering on a template. + * + * @return ArrayList + */ + public function getSummary() + { + $list = ArrayList::create(); + foreach ($this->searchParams as $searchField => $searchValue) { + if (empty($searchValue)) { + continue; + } + $filter = $this->getFilter($searchField); + if (!$filter) { + continue; + } + + $field = $this->fields->fieldByName($filter->getFullName()); + if (!$field) { + continue; + } + + // For dropdowns, checkboxes, etc, get the value that was presented to the user + // e.g. not an ID + if ($field instanceof SelectField) { + $source = $field->getSource(); + if (isset($source[$searchValue])) { + $searchValue = $source[$searchValue]; + } + } else { + // For checkboxes, it suffices to simply include the field in the list, since it's binary + if ($field instanceof CheckboxField) { + $searchValue = null; + } + } + + $list->push(ArrayData::create([ + 'Field' => $field->Title(), + 'Value' => $searchValue, + ])); + } + + return $list; + } } diff --git a/tests/php/ORM/Search/SearchContextTest.php b/tests/php/ORM/Search/SearchContextTest.php index ced8c07d1..4dfc3c77c 100644 --- a/tests/php/ORM/Search/SearchContextTest.php +++ b/tests/php/ORM/Search/SearchContextTest.php @@ -6,8 +6,11 @@ use SilverStripe\Dev\SapphireTest; use SilverStripe\Forms\TextField; use SilverStripe\Forms\TextareaField; use SilverStripe\Forms\NumericField; +use SilverStripe\Forms\DropdownField; +use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FieldList; use SilverStripe\ORM\Filters\PartialMatchFilter; +use SilverStripe\ORM\Search\SearchContext; class SearchContextTest extends SapphireTest { @@ -28,13 +31,13 @@ class SearchContextTest extends SapphireTest { $person = SearchContextTest\Person::singleton(); $context = $person->getDefaultSearchContext(); - $results = $context->getResults(array('Name'=>'')); + $results = $context->getResults(array('Name' => '')); $this->assertEquals(5, $results->Count()); - $results = $context->getResults(array('EyeColor'=>'green')); + $results = $context->getResults(array('EyeColor' => 'green')); $this->assertEquals(2, $results->Count()); - $results = $context->getResults(array('EyeColor'=>'green', 'HairColor'=>'black')); + $results = $context->getResults(array('EyeColor' => 'green', 'HairColor' => 'black')); $this->assertEquals(1, $results->Count()); } @@ -118,7 +121,7 @@ class SearchContextTest extends SapphireTest $project = singleton(SearchContextTest\Project::class); $context = $project->getDefaultSearchContext(); - $params = array("Name"=>"Blog Website", "Actions__SolutionArea"=>"technical"); + $params = array("Name" => "Blog Website", "Actions__SolutionArea" => "technical"); $results = $context->getResults($params); @@ -184,4 +187,67 @@ class SearchContextTest extends SapphireTest $this->assertEquals(1, $results->Count()); $this->assertEquals("Filtered value", $results->First()->HiddenValue); } + + public function testSearchContextSummary() + { + $filters = [ + 'KeywordSearch' => PartialMatchFilter::create('KeywordSearch'), + 'Country' => PartialMatchFilter::create('Country'), + 'CategoryID' => PartialMatchFilter::create('CategoryID'), + 'Featured' => PartialMatchFilter::create('Featured'), + 'Nothing' => PartialMatchFilter::create('Nothing'), + ]; + + $fields = FieldList::create( + TextField::create('KeywordSearch', 'Keywords'), + TextField::create('Country', 'Country'), + DropdownField::create('CategoryID', 'Category', [ + 1 => 'Category one', + 2 => 'Category two', + ]), + CheckboxField::create('Featured', 'Featured') + ); + + $context = SearchContext::create( + SearchContextTest\Person::class, + $fields, + $filters + ); + + $context->setSearchParams([ + 'KeywordSearch' => 'tester', + 'Country' => null, + 'CategoryID' => 2, + 'Featured' => 1, + 'Nothing' => 'empty', + ]); + + $list = $context->getSummary(); + + $this->assertEquals(3, $list->count()); + // KeywordSearch should be in the summary + $keyword = $list->find('Field', 'Keywords'); + $this->assertNotNull($keyword); + $this->assertEquals('tester', $keyword->Value); + + // Country should be skipped over + $country = $list->find('Field', 'Country'); + $this->assertNull($country); + + // Category should be expressed as the label + $category = $list->find('Field', 'Category'); + $this->assertNotNull($category); + $this->assertEquals('Category two', $category->Value); + + // Featured should have no value, since it's binary + $featured = $list->find('Field', 'Featured'); + $this->assertNotNull($featured); + $this->assertNull($featured->Value); + + // "Nothing" should come back null since there's no field for it + $nothing = $list->find('Field', 'Nothing'); + $this->assertNull($nothing); + + + } }