/**
  * @param array $data
  * @return mixed
  */
 public function results(array $data)
 {
     // do the search
     $results = ShopSearch::inst()->search($data);
     $request = $this->controller->getRequest();
     $baseLink = $request->getURL(false);
     // if there was only one category filter, remember it for the category dropdown to retain it's value
     if (!ShopSearchForm::config()->disable_category_dropdown) {
         $qs_filters = (string) Config::inst()->get('ShopSearch', 'qs_filters');
         $categoryKey = (string) ShopSearchForm::config()->category_field;
         if (preg_match('/\\[(.+)\\]/', $categoryKey, $matches)) {
             // get right of the f[] around the actual key if present
             $categoryKey = $matches[1];
         }
         if (!empty($data[$qs_filters][$categoryKey])) {
             $categoryID = $data[$qs_filters][$categoryKey];
             if (is_numeric($categoryID)) {
                 // If it's set in the dropdown it will just be a number
                 // If it's set from the checkboxes it will be something like LIST~1,2,3,4
                 // We only want to remember the value in the former case
                 Session::set('LastSearchCatID', $categoryID);
             }
         } else {
             // If they unchecked every value, then clear the dropdown as well
             Session::clear('LastSearchCatID');
         }
     }
     // add links for any facets
     if ($results->Facets && $results->Facets->count()) {
         $qs_ps = (string) Config::inst()->get('ShopSearch', 'qs_parent_search');
         $baseParams = array_merge($data, array($qs_ps => $results->SearchLogID));
         unset($baseParams['url']);
         $results->Facets = FacetHelper::inst()->insertFacetLinks($results->Facets, $baseParams, $baseLink);
     }
     // add a dropdown for sorting
     $qs_sort = (string) Config::inst()->get('ShopSearch', 'qs_sort');
     $options = Config::inst()->get('ShopSearch', 'sort_options');
     $sortParams = array_merge($data, array($qs_sort => 'NEWSORTVALUE'));
     unset($sortParams['url']);
     $results->SortControl = DropdownField::create($qs_sort, ShopSearch::config()->sort_label, $options, $results->Sort)->setAttribute('data-url', $baseLink . '?' . http_build_query($sortParams));
     // a little more output management
     $results->Title = "Search Results";
     $results->Results = $results->Matches;
     // this makes us compatible with the default search template
     // Give a hook for the parent controller to format the results, for example,
     // interpreting filters in a specific way to affect the title or content
     // when no results are returned. Since this is domain-specific we just leave
     // it up to the host app.
     if ($this->controller->hasMethod('onBeforeSearchDisplay')) {
         $this->controller->onBeforeSearchDisplay($results);
     }
     // give a hook for processing ajax requests through a different template (i.e. for returning only fragments)
     $tpl = Config::inst()->get('ShopSearch', 'ajax_results_template');
     if (!empty($tpl) && Director::is_ajax()) {
         return $this->controller->customise($results)->renderWith($tpl);
     }
     // Give a hook for modifying the search responses
     $this->controller->extend('updateSearchResultsResponse', $request, $response, $results, $data);
     return $response ?: $this->controller->customise($results)->renderWith(array('ShopSearch_results', 'Page_results', 'Page'));
 }
 /**
  * @param SS_HTTPRequest $req
  * @return string
  */
 public function search_suggest(SS_HTTPRequest $req)
 {
     /** @var SS_HTTPResponse $response */
     $response = $this->owner->getResponse();
     $callback = $req->requestVar('callback');
     // convert the search results into usable json for search-as-you-type
     if (ShopSearch::config()->search_as_you_type_enabled) {
         $searchVars = $req->requestVars();
         $searchVars[ShopSearch::config()->qs_query] = $searchVars['term'];
         unset($searchVars['term']);
         $results = ShopSearch::inst()->suggestWithResults($searchVars);
     } else {
         $results = array('suggestions' => ShopSearch::inst()->suggest($req->requestVar('term')));
     }
     if ($callback) {
         $response->addHeader('Content-type', 'application/javascript');
         $response->setBody($callback . '(' . json_encode($results) . ');');
     } else {
         $response->addHeader('Content-type', 'application/json');
         $response->setBody(json_encode($results));
     }
     return $response;
 }
 public function testFilters()
 {
     VirtualFieldIndex::build('Product');
     // one filter
     $r = ShopSearch::inst()->search(array('f' => array('Model' => 'ABC')));
     $this->assertEquals(2, $r->TotalMatches, 'Should contain 2 products');
     $this->assertEquals('ABC', $r->Matches->first()->Model, 'Should actually match');
     // two filters
     $r = ShopSearch::inst()->search(array('f' => array('Model' => 'ABC', 'Price' => 10.5)));
     $this->assertEquals(1, $r->TotalMatches, 'Should contain 1 product');
     $this->assertEquals('ABC', $r->Matches->first()->Model, 'Should actually match');
     $this->assertEquals(10.5, $r->Matches->first()->sellingPrice(), 'Should actually match');
     // filter on category
     $r = ShopSearch::inst()->search(array('f' => array('Category' => $this->idFromFixture('ProductCategory', 'c3'))));
     $this->assertEquals(3, $r->TotalMatches, 'Should contain 3 products');
     // filter on multiple categories
     $r = ShopSearch::inst()->search(array('f' => array('Category' => array($this->idFromFixture('ProductCategory', 'c1'), $this->idFromFixture('ProductCategory', 'c3')))));
     $this->assertEquals(4, $r->TotalMatches, 'Should contain all products');
     // filter on multiple categories with comma separation
     $r = ShopSearch::inst()->search(array('f' => array('Category' => 'LIST~' . implode(',', array($this->idFromFixture('ProductCategory', 'c1'), $this->idFromFixture('ProductCategory', 'c3'))))));
     $this->assertEquals(4, $r->TotalMatches, 'Should contain all products');
     // filter on price range
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~8~12')));
     $this->assertEquals(1, $r->TotalMatches, 'Should contain only 1 product');
     $this->assertEquals($this->idFromFixture('Product', 'p2'), $r->Matches->first()->ID, 'Match should be p2');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~-3~4')));
     $this->assertEquals(0, $r->TotalMatches, 'Empty matches work on the low end');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~5555~10000')));
     $this->assertEquals(0, $r->TotalMatches, 'Empty matches work on the high end');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~12~8')));
     $this->assertEquals(0, $r->TotalMatches, 'A flipped range does not cause error');
 }