New getSummary() API for SearchContext

This commit is contained in:
Aaron Carlino 2017-07-26 16:35:10 +12:00 committed by Damian Mooyman
parent 18863f0916
commit 74873096bd
2 changed files with 176 additions and 37 deletions

View File

@ -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;
}
}

View File

@ -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);
}
}