Completed
Push — master ( 08af57...01b1ac )
by Mark
17:55
created

ShopSearch::get_searchable_classes()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 6.6038
cc 8
eloc 12
nc 9
nop 0
1
<?php
2
/**
3
 * Fulltext search index for shop buyables
4
 *
5
 * @author Mark Guinn <[email protected]>
6
 * @date 08.29.2013
7
 * @package shop_search
8
 */
9
class ShopSearch extends Object
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
10
{
11
    const FACET_TYPE_LINK       = 'link';
12
    const FACET_TYPE_CHECKBOX   = 'checkbox';
13
    const FACET_TYPE_RANGE      = 'range';
14
15
    /** @var string - class name of adapter class to use */
16
    private static $adapter_class = 'ShopSearchSimple';
0 ignored issues
show
Unused Code introduced by
The property $adapter_class is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
17
18
    /** @var array - these classes will be added to the index - e.g. Category, Page, etc. */
19
    private static $searchable = array();
0 ignored issues
show
Unused Code introduced by
The property $searchable is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
20
21
    /** @var bool - if true, all buyable models will be added to the index automatically  */
22
    private static $buyables_are_searchable = true;
0 ignored issues
show
Unused Code introduced by
The property $buyables_are_searchable is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
23
24
    /** @var int - size of paging in the search */
25
    private static $page_size = 10;
0 ignored issues
show
Unused Code introduced by
The property $page_size is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
26
27
    /** @var bool */
28
    private static $suggest_enabled = true;
0 ignored issues
show
Unused Code introduced by
The property $suggest_enabled is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
29
30
    /** @var int - how many suggestions to provide */
31
    private static $suggest_limit = 5;
0 ignored issues
show
Unused Code introduced by
The property $suggest_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
32
33
    /** @var bool */
34
    private static $search_as_you_type_enabled = true;
0 ignored issues
show
Unused Code introduced by
The property $search_as_you_type_enabled is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
35
36
    /** @var int - how may sayt (search-as-you-type) entries to provide */
37
    private static $sayt_limit = 5;
0 ignored issues
show
Unused Code introduced by
The property $sayt_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
38
39
    /** @var bool - automatically create facets for static attributes */
40
    private static $auto_facet_attributes = false;
0 ignored issues
show
Unused Code introduced by
The property $auto_facet_attributes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
    /** @var string - optionally, a different template to run ajax results through (sans-Page.ss) */
43
    private static $ajax_results_template = '';
0 ignored issues
show
Unused Code introduced by
The property $ajax_results_template is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
44
45
    /** @var string - these allow you to use different querystring params in you need to */
46
    private static $qs_query         = 'q';
0 ignored issues
show
Unused Code introduced by
The property $qs_query is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
47
    private static $qs_filters       = 'f';
0 ignored issues
show
Unused Code introduced by
The property $qs_filters is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
48
    private static $qs_parent_search = '__ps';
0 ignored issues
show
Unused Code introduced by
The property $qs_parent_search is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
49
    private static $qs_title         = '__t';
0 ignored issues
show
Unused Code introduced by
The property $qs_title is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
    private static $qs_source        = '__src'; // used to log searches from search-as-you-type
0 ignored issues
show
Unused Code introduced by
The property $qs_source is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
51
    private static $qs_sort          = 'sort';
0 ignored issues
show
Unused Code introduced by
The property $qs_sort is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
52
53
    /** @var array - I'm leaving this particularly bare b/c with config merging it's a pain to remove items */
54
    private static $sort_options = array(
0 ignored issues
show
Unused Code introduced by
The property $sort_options is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
55
        'score desc'            => 'Relevance',
56
//		'SiteTree_Title asc'    => 'Alphabetical (A-Z)',
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
57
//		'SiteTree_Title dsc'    => 'Alphabetical (Z-A)',
58
    );
59
60
    /**
61
     * @var array - default search facets (price, category, etc)
62
     *   Key    field name - e.g. Price - can be a VirtualFieldIndex field
63
     *   Value  facet label - e.g. Search By Category - if the value is a relation or returns an array or
64
     *          list all values will be faceted individually
65
     *          NOTE: this can also be another array with keys: Label, Type, and Values (for checkbox only)
66
     */
67
    private static $facets = array();
0 ignored issues
show
Unused Code introduced by
The property $facets is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
68
69
    /** @var array - field definition for Solr only */
70
    private static $solr_fulltext_fields = array();
0 ignored issues
show
Unused Code introduced by
The property $solr_fulltext_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
71
72
    /** @var array - field definition for Solr only */
73
    private static $solr_filter_fields = array();
0 ignored issues
show
Unused Code introduced by
The property $solr_filter_fields is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
74
75
    /** @var string - if present, will create a copy of SiteTree_Title that's suited for alpha sorting */
76
    private static $solr_title_sort_field = '';
0 ignored issues
show
Unused Code introduced by
The property $solr_title_sort_field is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
77
78
    /**
79
     * @var string - If present, everything matching the following regex will be removed from
80
     *               keyword search queries before passing to the search adapter.
81
     */
82
    private static $keyword_filter_regex = '/[^a-zA-Z0-9\s\-]/';
0 ignored issues
show
Unused Code introduced by
The property $keyword_filter_regex is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
83
84
85
    /**
86
     * @return array
87
     */
88
    public static function get_searchable_classes()
89
    {
90
        // First get any explicitly declared searchable classes
91
        $searchable = Config::inst()->get('ShopSearch', 'searchable');
92
        if (is_string($searchable) && strlen($searchable) > 0) {
93
            $searchable = array($searchable);
94
        } elseif (!is_array($searchable)) {
95
            $searchable = array();
96
        }
97
98
        // Add in buyables automatically if asked
99
        if (Config::inst()->get('ShopSearch', 'buyables_are_searchable')) {
100
            $buyables = SS_ClassLoader::instance()->getManifest()->getImplementorsOf('Buyable');
101
            if (is_array($buyables) && count($buyables) > 0) {
102
                foreach ($buyables as $c) {
103
                    $searchable[] = $c;
104
                }
105
            }
106
        }
107
108
        return array_unique($searchable);
109
    }
110
111
    /**
112
     * Returns an array of categories suitable for a dropdown menu
113
     * TODO: cache this
114
     *
115
     * @param int $parentID [optional]
116
     * @param string $prefix [optional]
117
     * @param int $maxDepth [optional]
118
     * @return array
119
     * @static
120
     */
121
    public static function get_category_hierarchy($parentID = 0, $prefix = '', $maxDepth = 999)
122
    {
123
        $out = array();
124
        $cats = ProductCategory::get()
125
            ->filter(array(
126
                'ParentID'      => $parentID,
127
                'ShowInMenus'   => 1,
128
            ))
129
            ->sort('Sort');
130
131
        // If there is a single parent category (usually "Products" or something), we
132
        // probably don't want that in the hierarchy.
133
        if ($parentID == 0 && $cats->count() == 1) {
134
            return self::get_category_hierarchy($cats->first()->ID, $prefix, $maxDepth);
135
        }
136
137
        foreach ($cats as $cat) {
138
            $out[$cat->ID] = $prefix . $cat->Title;
139
            if ($maxDepth > 1) {
140
                $out += self::get_category_hierarchy($cat->ID, $prefix . $cat->Title . ' > ', $maxDepth - 1);
141
            }
142
        }
143
144
        return $out;
145
    }
146
147
    /**
148
     * @return ShopSearchAdapter
149
     */
150
    public static function adapter()
151
    {
152
        $adapterClass = Config::inst()->get('ShopSearch', 'adapter_class');
153
        return Injector::inst()->get($adapterClass);
154
    }
155
156
    /**
157
     * @return ShopSearch
158
     */
159
    public static function inst()
160
    {
161
        return Injector::inst()->get('ShopSearch');
162
    }
163
164
    /**
165
     * The result will contain at least the following:
166
     *      Matches - SS_List of results
167
     *      TotalMatches - total # of results, unlimited
168
     *      Query - query string
169
     * Also saves a log record.
170
     *
171
     * @param array $vars
172
     * @param bool $logSearch [optional]
173
     * @param bool $useFacets [optional]
174
     * @param int $start [optional]
175
     * @param int $limit [optional]
176
     * @return ArrayData
177
     */
178
    public function search(array $vars, $logSearch=true, $useFacets=true, $start=-1, $limit=-1)
179
    {
180
        $qs_q   = $this->config()->get('qs_query');
181
        $qs_f   = $this->config()->get('qs_filters');
182
        $qs_ps  = $this->config()->get('qs_parent_search');
183
        $qs_t   = $this->config()->get('qs_title');
184
        $qs_sort= $this->config()->get('qs_sort');
185
        if ($limit < 0) {
186
            $limit  = $this->config()->get('page_size');
187
        }
188
        if ($start < 0) {
189
            $start  = !empty($vars['start']) ? (int)$vars['start'] : 0;
190
        } // as far as i can see, fulltextsearch hard codes 'start'
191
        $facets = $useFacets ? $this->config()->get('facets') : array();
192
        if (!is_array($facets)) {
193
            $facets = array();
194
        }
195
        if (empty($limit)) {
196
            $limit = -1;
197
        }
198
199
        // figure out and scrub the sort
200
        $sortOptions = $this->config()->get('sort_options');
201
        $sort        = !empty($vars[$qs_sort]) ? $vars[$qs_sort] : '';
202
        if (!isset($sortOptions[$sort])) {
203
            $sort    = current(array_keys($sortOptions));
204
        }
205
206
        // figure out and scrub the filters
207
        $filters  = !empty($vars[$qs_f]) ? FacetHelper::inst()->scrubFilters($vars[$qs_f]) : array();
208
209
        // do the search
210
        $keywords = !empty($vars[$qs_q]) ? $vars[$qs_q] : '';
211
        if ($keywordRegex = $this->config()->get('keyword_filter_regex')) {
212
            $keywords = preg_replace($keywordRegex, '', $keywords);
213
        }
214
        $results  = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
215
216
        // massage the results a bit
217
        if (!empty($keywords) && !$results->hasValue('Query')) {
218
            $results->Query = $keywords;
219
        }
220
        if (!empty($filters) && !$results->hasValue('Filters')) {
221
            $results->Filters = new ArrayData($filters);
222
        }
223
        if (!$results->hasValue('Sort')) {
224
            $results->Sort = $sort;
225
        }
226
        if (!$results->hasValue('TotalMatches')) {
227
            $results->TotalMatches = $results->Matches->hasMethod('getTotalItems')
228
                ? $results->Matches->getTotalItems()
229
                : $results->Matches->count();
230
        }
231
232
        // for some types of facets, update the state
233
        if ($results->hasValue('Facets')) {
234
            FacetHelper::inst()->transformHierarchies($results->Facets);
235
            FacetHelper::inst()->updateFacetState($results->Facets, $filters);
236
        }
237
238
        // make a hash of the search so we can know if we've already logged it this session
239
        $loggedFilters = !empty($filters) ? json_encode($filters) : null;
240
        $loggedQuery   = strtolower($results->Query);
241
//		$searchHash    = md5($loggedFilters . $loggedQuery);
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
242
//		$sessSearches  = Session::get('loggedSearches');
243
//		if (!is_array($sessSearches)) $sessSearches = array();
244
//		Debug::dump($searchHash, $sessSearches);
245
246
        // save the log record
247
        if ($start == 0 && $logSearch && (!empty($keywords) || !empty($filters))) { // && !in_array($searchHash, $sessSearches)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
248
            $log = SearchLog::create(array(
249
                'Query'         => $loggedQuery,
250
                'Title'         => !empty($vars[$qs_t]) ? $vars[$qs_t] : '',
251
                'Link'          => Controller::curr()->getRequest()->getURL(true), // I'm not 100% happy with this, but can't think of a better way
252
                'NumResults'    => $results->TotalMatches,
253
                'MemberID'      => Member::currentUserID(),
254
                'Filters'       => $loggedFilters,
255
                'ParentSearchID'=> !empty($vars[$qs_ps]) ? $vars[$qs_ps] : 0,
256
            ));
257
            $log->write();
258
            $results->SearchLogID = $log->ID;
259
            $results->SearchBreadcrumbs = $log->getBreadcrumbs();
260
261
//			$sessSearches[] = $searchHash;
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
262
//			Session::set('loggedSearches', $sessSearches);
263
        }
264
265
        return $results;
266
    }
267
268
    /**
269
     * @param string $str
270
     * @return SS_Query
271
     */
272
    public function getSuggestQuery($str='')
273
    {
274
        $hasResults = 'CASE WHEN max("SearchLog"."NumResults") > 0 THEN 1 ELSE 0 END';
275
        $searchCount = 'count(distinct "SearchLog"."ID")';
276
        $q = new SQLQuery();
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
277
        $q = $q->setSelect('"SearchLog"."Query"')
278
            // TODO: what to do with filter?
279
            ->selectField($searchCount, 'SearchCount')
280
            ->selectField('max("SearchLog"."Created")', 'LastSearch')
281
            ->selectField('max("SearchLog"."NumResults")', 'NumResults')
282
            ->selectField($hasResults, 'HasResults')
283
            ->setFrom('"SearchLog"')
284
            ->setGroupBy('"SearchLog"."Query"')
285
            ->setOrderBy(array(
286
                "$hasResults DESC",
287
                "$searchCount DESC"
288
            ))
289
            ->setLimit(Config::inst()->get('ShopSearch', 'suggest_limit'))
290
        ;
291
292
        if (strlen($str) > 0) {
293
            $q = $q->addWhere(sprintf('"SearchLog"."Query" LIKE \'%%%s%%\'', Convert::raw2sql($str)));
294
        }
295
296
        return $q;
297
    }
298
299
300
    /**
301
     * @param string $str
302
     * @return array
303
     */
304
    public function suggest($str='')
305
    {
306
        $adapter = self::adapter();
307
        if ($adapter->hasMethod('suggest')) {
308
            return $adapter->suggest($str);
0 ignored issues
show
Bug introduced by
The method suggest() does not seem to exist on object<ShopSearchAdapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
309
        } else {
310
            return $this->getSuggestQuery($str)->execute()->column('Query');
311
        }
312
    }
313
314
315
    /**
316
     * Returns an array that can be made into json and passed to the controller
317
     * containing both term suggestions and a few product matches.
318
     *
319
     * @param array $searchVars
320
     * @return array
321
     */
322
    public function suggestWithResults(array $searchVars)
323
    {
324
        $qs_q       = $this->config()->get('qs_query');
325
        $qs_f       = $this->config()->get('qs_filters');
326
        $keywords   = !empty($searchVars[$qs_q]) ? $searchVars[$qs_q] : '';
327
        $filters    = !empty($searchVars[$qs_f]) ? $searchVars[$qs_f] : array();
328
329
        $adapter = self::adapter();
330
331
        // get suggestions and product list from the adapter
332
        if ($adapter->hasMethod('suggestWithResults')) {
333
            $results = $adapter->suggestWithResults($keywords, $filters);
334
        } else {
335
            $limit      = (int)ShopSearch::config()->sayt_limit;
336
            $search     = self::adapter()->searchFromVars($keywords, $filters, array(), 0, $limit, 'Popularity DESC');
337
            //$search     = ShopSearch::inst()->search($searchVars, false, false, 0, $limit);
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
338
339
            $results = array(
340
                'products'      => $search->Matches,
341
                'suggestions'   => $this->suggest($keywords),
342
            );
343
        }
344
345
        // the adapter just gave us a list of products, which we need to process a little further
346
        if (!empty($results['products'])) {
347
            // this gets encoded into the product links
348
            $searchVars['total'] = $results['products']->hasMethod('getTotalItems')
349
                ? $results['products']->getTotalItems()
350
                : $results['products']->count();
351
352
            $products   = array();
353
            foreach ($results['products'] as $prod) {
354
                if (!$prod || !$prod->exists()) {
355
                    continue;
356
                }
357
                $img = $prod->hasMethod('ProductImage') ? $prod->ProductImage() : $prod->Image();
358
                $thumb = ($img && $img->exists()) ? $img->getThumbnail() : null;
359
360
                $json = array(
361
                    'link'  => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))),
362
                    'title' => $prod->Title,
363
                    'desc'  => $prod->obj('Content')->Summary(),
364
                    'thumb' => $thumb ? $thumb->Link() : '',
365
                    'price' => $prod->obj('Price')->Nice(),
366
                );
367
368
                if ($prod->hasExtension('HasPromotionalPricing') && $prod->hasValidPromotion()) {
369
                    $json['original_price'] = $prod->getOriginalPrice()->Nice();
370
                }
371
372
                $products[] = $json;
373
            }
374
375
            // replace the list of product objects with json
376
            $results['products'] = $products;
377
        }
378
379
        $this->extend('updateSuggestWithResults', $results, $keywords, $filters);
380
381
        return $results;
382
    }
383
}
384