FacetHelper::getRecursiveChildValues()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 13
ccs 0
cts 9
cp 0
rs 9.4285
cc 3
eloc 7
nc 3
nop 1
crap 12
1
<?php
2
/**
3
 * Adds methods for limited kinds of faceting using the silverstripe ORM.
4
 * This is used by the default ShopSearchSimple adapter but also can
5
 * be added to other contexts (such as ProductCategory).
6
 *
7
 * TODO: Facet class + subclasses
8
 *
9
 * @author Mark Guinn <[email protected]>
10
 * @date 10.21.2013
11
 * @package shop_search
12
 * @subpackage helpers
13
 */
14
class FacetHelper 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...
15
{
16
    /** @var bool - if this is turned on it will use an algorithm that doesn't require traversing the data set if possible */
17
    private static $faster_faceting = false;
0 ignored issues
show
Unused Code introduced by
The property $faster_faceting 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...
18
19
    /** @var bool - should the facets (link and checkbox only) be sorted - this can mess with things like category lists */
20
    private static $sort_facet_values = true;
0 ignored issues
show
Unused Code introduced by
The property $sort_facet_values 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...
21
22
    /** @var string - I don't know why you'd want to override this, but you could if you wanted */
23
    private static $attribute_facet_regex = '/^ATT(\d+)$/';
0 ignored issues
show
Unused Code introduced by
The property $attribute_facet_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...
24
25
    /** @var bool - For checkbox facets, is the initial state all checked or all unchecked? */
26
    private static $default_checkbox_state = true;
0 ignored issues
show
Unused Code introduced by
The property $default_checkbox_state 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...
27
28
29
    /**
30
     * @return FacetHelper
31
     */
32 7
    public static function inst()
33
    {
34 7
        return Injector::inst()->get('FacetHelper');
35
    }
36
37
38
    /**
39
     * Performs some quick pre-processing on filters from any source
40
     *
41
     * @param array $filters
42
     * @return array
43
     */
44 2
    public function scrubFilters($filters)
45
    {
46 2
        if (!is_array($filters)) {
47
            $filters = array();
48
        }
49
50 2
        foreach ($filters as $k => $v) {
51 2
            if (empty($v)) {
52
                unset($filters[$k]);
53
            }
54
            // this allows you to send an array as a comma-separated list, which is easier on the query string length
55 2
            if (is_string($v) && strpos($v, 'LIST~') === 0) {
56 1
                $filters[$k] = explode(',', substr($v, 5));
57 1
            }
58 2
        }
59
60 2
        return $filters;
61
    }
62
63
64
    /**
65
     * @param DataList $list
66
     * @param array    $filters
67
     * @param DataObject|string $sing - just a singleton object we can get information off of
68
     * @return DataList
69
     */
70 7
    public function addFiltersToDataList($list, array $filters, $sing=null)
71
    {
72 7
        if (!$sing) {
73 7
            $sing = singleton($list->dataClass());
74 7
        }
75 7
        if (is_string($sing)) {
76
            $sing = singleton($sing);
77
        }
78
79 7
        if (!empty($filters)) {
80 4
            foreach ($filters as $filterField => $filterVal) {
81 4
                if ($sing->hasExtension('HasStaticAttributes') && preg_match(self::config()->attribute_facet_regex, $filterField, $matches)) {
82
                    //					$sav = $sing->StaticAttributeValues();
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...
83
//					Debug::log("sav = {$sav->getJoinTable()}, {$sav->getLocalKey()}, {$sav->getForeignKey()}");
84
//					$list = $list
85
//						->innerJoin($sav->getJoinTable(), "\"{$sing->baseTable()}\".\"ID\" = \"{$sav->getJoinTable()}\".\"{$sav->getLocalKey()}\"")
86
//						->filter("\"{$sav->getJoinTable()}\".\"{$sav->getForeignKey()}\"", $filterVal)
87
//					;
88
                    // TODO: This logic should be something like the above, but I don't know
89
                    // how to get the join table from a singleton (which returns an UnsavedRelationList
90
                    // instead of a ManyManyList). I've got a deadline to meet, though, so this
91
                    // will catch the majority of cases as long as the extension is applied to the
92
                    // Product class instead of a subclass.
93
                    $list = $list
94 1
                        ->innerJoin('Product_StaticAttributeTypes', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeTypes\".\"ProductID\"")
95 1
                        ->innerJoin('ProductAttributeValue', "\"Product_StaticAttributeTypes\".\"ProductAttributeTypeID\" = \"ProductAttributeValue\".\"TypeID\"")
96 1
                        ->innerJoin('Product_StaticAttributeValues', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductID\" AND \"ProductAttributeValue\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductAttributeValueID\"")
97 1
                        ->filter("Product_StaticAttributeValues.ProductAttributeValueID", $filterVal);
98 1
                } else {
99 3
                    $list = $list->filter($this->processFilterField($sing, $filterField, $filterVal));
100
                }
101 4
            }
102 4
        }
103
104 7
        return $list;
105
    }
106
107
108
    /**
109
     * @param DataObject $rec           This would normally just be a singleton but we don't want to have to create it over and over
110
     * @param string     $filterField
111
     * @param mixed      $filterVal
112
     * @return array - returns the new filter added
113
     */
114 3
    public function processFilterField($rec, $filterField, $filterVal)
115
    {
116
        // First check for VFI fields
117 3
        if ($rec->hasExtension('VirtualFieldIndex') && ($spec = $rec->getVFISpec($filterField))) {
118 3
            if ($spec['Type'] == VirtualFieldIndex::TYPE_LIST) {
119
                // Lists have to be handled a little differently
120 1
                $f = $rec->getVFIFieldName($filterField) . ':PartialMatch';
121 1
                if (is_array($filterVal)) {
122 1
                    foreach ($filterVal as &$val) {
123 1
                        $val = '|' . $val . '|';
124 1
                    }
125 1
                    return array($f => $filterVal);
126
                } else {
127 1
                    return array($f => '|' . $filterVal . '|');
128
                }
129
            } else {
130
                // Simples are simple
131 3
                $filterField = $rec->getVFIFieldName($filterField);
132
            }
133 3
        }
134
135
        // Next check for regular db fields
136 3
        if ($rec->dbObject($filterField)) {
137
            // Is it a range value?
138 3
            if (is_string($filterVal) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filterVal, $m)) {
139 2
                $filterField .= ':Between';
140 2
                $filterVal = array_slice($m, 1, 2);
141 2
            }
142
143 3
            return array($filterField => $filterVal);
144
        }
145
146
        return array();
147
    }
148
149
150
    /**
151
     * Processes the facet spec and removes any shorthand (field => label).
152
     * @param array $facetSpec
153
     * @return array
154
     */
155 7
    public function expandFacetSpec(array $facetSpec)
156
    {
157 7
        if (is_null($facetSpec)) {
158
            return array();
159
        }
160 7
        $facets = array();
161
162 7
        foreach ($facetSpec as $field => $label) {
163 3
            if (is_array($label)) {
164 3
                $facets[$field] = $label;
165 3
            } else {
166 1
                $facets[$field] = array('Label' => $label);
167
            }
168
169 3
            if (empty($facets[$field]['Source'])) {
170 3
                $facets[$field]['Source'] = $field;
171 3
            }
172 3
            if (empty($facets[$field]['Type'])) {
173 1
                $facets[$field]['Type']  = ShopSearch::FACET_TYPE_LINK;
174 1
            }
175
176 3
            if (empty($facets[$field]['Values'])) {
177 3
                $facets[$field]['Values'] = array();
178 3
            } else {
179 2
                $vals = $facets[$field]['Values'];
180 2
                if (is_string($vals)) {
181 2
                    $vals = eval('return ' . $vals . ';');
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
182 2
                }
183 2
                $facets[$field]['Values'] = array();
184 2
                foreach ($vals as $val => $lbl) {
185 2
                    $facets[$field]['Values'][$val] = new ArrayData(array(
186 2
                        'Label'     => $lbl,
187 2
                        'Value'     => $val,
188 2
                        'Count'     => 0,
189 2
                    ));
190 2
                }
191
            }
192 7
        }
193
194 7
        return $facets;
195
    }
196
197
198
    /**
199
     * This is super-slow. I'm assuming if you're using facets you
200
     * probably also ought to be using Solr or something else. Or
201
     * maybe you have unlimited time and can refactor this feature
202
     * and submit a pull request...
203
     *
204
     * TODO: If this is going to be used for categories we're going
205
     * to have to really clean it up and speed it up.
206
     * Suggestion:
207
     *  - option to turn off counts
208
     *  - switch order of nested array so we don't go through results unless needed
209
     *  - if not doing counts, min/max and link facets can be handled w/ queries
210
     *  - separate that bit out into a new function
211
     * NOTE: This is partially done with the "faster_faceting" config
212
     * option but more could be done, particularly by covering link facets as well.
213
     *
214
     * Output - list of ArrayData in the format:
215
     *   Label - name of the facet
216
     *   Source - field name of the facet
217
     *   Type - one of the ShopSearch::FACET_TYPE_XXXX constants
218
     *   Values - SS_List of possible values for this facet
219
     *
220
     * @param SS_List $matches
221
     * @param array $facetSpec
222
     * @param bool $autoFacetAttributes [optional]
223
     * @return ArrayList
224
     */
225 7
    public function buildFacets(SS_List $matches, array $facetSpec, $autoFacetAttributes=false)
226
    {
227 7
        $facets = $this->expandFacetSpec($facetSpec);
228 7
        if (!$autoFacetAttributes && (empty($facets) || !$matches || !$matches->count())) {
229 4
            return new ArrayList();
230
        }
231 3
        $fasterMethod = (bool)$this->config()->faster_faceting;
232
233
        // fill them in
234 3
        foreach ($facets as $field => &$facet) {
235 3
            if (preg_match(self::config()->attribute_facet_regex, $field, $m)) {
236 1
                $this->buildAttributeFacet($matches, $facet, $m[1]);
0 ignored issues
show
Documentation introduced by
$matches is of type object<SS_List>, but the function expects a object<DataList>|object<PaginatedList>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
237 1
                continue;
238
            }
239
240
            // NOTE: using this method range and checkbox facets don't get counts
241 2
            if ($fasterMethod && $facet['Type'] != ShopSearch::FACET_TYPE_LINK) {
242 View Code Duplication
                if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
                    if (isset($facet['RangeMin'])) {
244
                        $facet['MinValue'] = $facet['RangeMin'];
245
                    }
246
                    if (isset($facet['RangeMax'])) {
247
                        $facet['MaxValue'] = $facet['RangeMax'];
248
                    }
249
                }
250
251
                continue;
252
            }
253
254 2
            foreach ($matches as $rec) {
255
                // If it's a range facet, set up the min/max
256 2 View Code Duplication
                if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257 2
                    if (isset($facet['RangeMin'])) {
258 2
                        $facet['MinValue'] = $facet['RangeMin'];
259 2
                    }
260 2
                    if (isset($facet['RangeMax'])) {
261 2
                        $facet['MaxValue'] = $facet['RangeMax'];
262 2
                    }
263 2
                }
264
265
                // If the field is accessible via normal methods, including
266
                // a user-defined getter, prefer that
267 2
                $fieldValue = $rec->relObject($field);
268 2
                if (is_null($fieldValue) && $rec->hasMethod($meth = "get{$field}")) {
269 2
                    $fieldValue = $rec->$meth();
270 2
                }
271
272
                // If not, look for a VFI field
273 2
                if (!$fieldValue && $rec->hasExtension('VirtualFieldIndex')) {
274 2
                    $fieldValue = $rec->getVFI($field);
275 2
                }
276
277
                // If we found something, process it
278 2
                if (!empty($fieldValue)) {
279
                    // normalize so that it's iterable
280 2
                    if (!is_array($fieldValue) && !$fieldValue instanceof SS_List) {
281 2
                        $fieldValue = array($fieldValue);
282 2
                    }
283
284 2
                    foreach ($fieldValue as $obj) {
285 2
                        if (empty($obj)) {
286
                            continue;
287
                        }
288
289
                        // figure out the right label
290 2
                        if (is_object($obj) && $obj->hasMethod('Nice')) {
291
                            $lbl = $obj->Nice();
292 2
                        } elseif (is_object($obj) && !empty($obj->Title)) {
293 2
                            $lbl = $obj->Title;
294 2
                        } elseif (
295 2
                            is_numeric($obj) &&
296 2
                            !empty($facet['LabelFormat']) &&
297 2
                            $facet['LabelFormat'] === 'Currency' &&
298 1
                            $facet['Type'] !== ShopSearch::FACET_TYPE_RANGE // this one handles it via javascript
299 2
                        ) {
300 1
                            $tmp = Currency::create($field);
301 1
                            $tmp->setValue($obj);
302 1
                            $lbl = $tmp->Nice();
303 1
                        } else {
304 2
                            $lbl = (string)$obj;
305
                        }
306
307
                        // figure out the value for sorting
308 2
                        if (is_object($obj) && $obj->hasMethod('getAmount')) {
309
                            $val = $obj->getAmount();
310 2
                        } elseif (is_object($obj) && !empty($obj->ID)) {
311 2
                            $val = $obj->ID;
312 2
                        } else {
313 2
                            $val = (string)$obj;
314
                        }
315
316
                        // if it's a range facet, calculate the min and max
317 2
                        if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
318 2
                            if (!isset($facet['MinValue']) || $val < $facet['MinValue']) {
319 2
                                $facet['MinValue'] = $val;
320 2
                                $facet['MinLabel'] = $lbl;
321 2
                            }
322 2
                            if (!isset($facet['RangeMin']) || $val < $facet['RangeMin']) {
323 2
                                $facet['RangeMin'] = $val;
324 2
                            }
325 2
                            if (!isset($facet['MaxValue']) || $val > $facet['MaxValue']) {
326 2
                                $facet['MaxValue'] = $val;
327 2
                                $facet['MaxLabel'] = $lbl;
328 2
                            }
329 2
                            if (!isset($facet['RangeMax']) || $val > $facet['RangeMax']) {
330 2
                                $facet['RangeMax'] = $val;
331 2
                            }
332 2
                        }
333
334
                        // Tally the value in the facets
335 2
                        if (!isset($facet['Values'][$val])) {
336 2
                            $facet['Values'][$val] = new ArrayData(array(
337 2
                                'Label'     => $lbl,
338 2
                                'Value'     => $val,
339 2
                                'Count'     => 1,
340 2
                            ));
341 2
                        } elseif ($facet['Values'][$val]) {
342 2
                            $facet['Values'][$val]->Count++;
343 2
                        }
344 2
                    }
345 2
                }
346 2
            }
347 3
        }
348
349
        // if we're auto-building the facets based on attributes,
350 3
        if ($autoFacetAttributes) {
351 1
            $facets = array_merge($this->buildAllAttributeFacets($matches), $facets);
0 ignored issues
show
Documentation introduced by
$matches is of type object<SS_List>, but the function expects a object<DataList>|object<PaginatedList>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
352 1
        }
353
354
        // convert values to arraylist
355 3
        $out = new ArrayList();
356 3
        $sortValues = self::config()->sort_facet_values;
357 3
        foreach ($facets as $f) {
358 3
            if ($sortValues) {
359 3
                ksort($f['Values']);
360 3
            }
361 3
            $f['Values'] = new ArrayList($f['Values']);
362 3
            $out->push(new ArrayData($f));
363 3
        }
364
365 3
        return $out;
366
    }
367
368
369
    /**
370
     * NOTE: this will break if applied to something that's not a SiteTree subclass.
371
     * @param DataList|PaginatedList $matches
372
     * @param array $facet
373
     * @param int $typeID
374
     */
375 1
    protected function buildAttributeFacet($matches, array &$facet, $typeID)
376
    {
377 1
        $q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method dataQuery() does only exist in the following implementations of said interface: DataList, HasManyList, ManyManyList, Member_GroupSet, OrderItemList, PolymorphicHasManyList, RelationList, UnsavedRelationList.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
378
379 1
        if (empty($facet['Label'])) {
380
            $type = ProductAttributeType::get()->byID($typeID);
381
            $facet['Label'] = $type->Label;
382
        }
383
384 1
        $baseTable = $q->getFrom();
385 1
        if (is_array($baseTable)) {
386 1
            $baseTable = reset($baseTable);
387 1
        }
388
389 1
        $q = $q->setSelect(array())
390 1
            ->selectField('"ProductAttributeValue"."ID"', 'Value')
391 1
            ->selectField('"ProductAttributeValue"."Value"', 'Label')
392 1
            ->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
393 1
            ->selectField('"ProductAttributeValue"."Sort"')
394 1
            ->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
395 1
            ->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"')
396 1
            ->addWhere(sprintf("\"ProductAttributeValue\".\"TypeID\" = '%d'", $typeID))
397 1
            ->setOrderBy('"ProductAttributeValue"."Sort"', 'ASC')
398 1
            ->setGroupBy('"ProductAttributeValue"."ID"')
399 1
            ->execute()
400 1
        ;
401
402 1
        $facet['Values'] = array();
403 1
        foreach ($q as $row) {
404 1
            $facet['Values'][ $row['Value'] ] = new ArrayData($row);
405 1
        }
406 1
    }
407
408
409
    /**
410
     * Builds facets from all attributes present in the data set.
411
     * @param DataList|PaginatedList $matches
412
     * @return array
413
     */
414 1
    protected function buildAllAttributeFacets($matches)
415
    {
416 1
        $q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SS_List as the method dataQuery() does only exist in the following implementations of said interface: DataList, HasManyList, ManyManyList, Member_GroupSet, OrderItemList, PolymorphicHasManyList, RelationList, UnsavedRelationList.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
417
418
        // this is the easiest way to get SiteTree vs SiteTree_Live
419 1
        $baseTable = $q->getFrom();
420 1
        if (is_array($baseTable)) {
421 1
            $baseTable = reset($baseTable);
422 1
        }
423
424 1
        $q = $q->setSelect(array())
425 1
            ->selectField('"ProductAttributeType"."ID"', 'TypeID')
426 1
            ->selectField('"ProductAttributeType"."Label"', 'TypeLabel')
427 1
            ->selectField('"ProductAttributeValue"."ID"', 'Value')
428 1
            ->selectField('"ProductAttributeValue"."Value"', 'Label')
429 1
            ->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
430 1
            ->selectField('"ProductAttributeValue"."Sort"')
431 1
            ->addInnerJoin('Product_StaticAttributeTypes', $baseTable.'."ID" = "Product_StaticAttributeTypes"."ProductID"')
432 1
            ->addInnerJoin('ProductAttributeType', '"Product_StaticAttributeTypes"."ProductAttributeTypeID" = "ProductAttributeType"."ID"')
433 1
            ->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
434 1
            ->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"'
435 1
                . ' AND "ProductAttributeValue"."TypeID" = "ProductAttributeType"."ID"')
436 1
            ->setOrderBy(array(
437 1
                '"ProductAttributeType"."Label"' => 'ASC',
438 1
                '"ProductAttributeValue"."Sort"' => 'ASC',
439 1
            ))
440 1
            ->setGroupBy(array('"ProductAttributeValue"."ID"', '"ProductAttributeType"."ID"'))
441 1
            ->execute()
442 1
        ;
443
444
445 1
        $curType  = 0;
446 1
        $facets   = array();
447 1
        $curFacet = null;
448 1
        foreach ($q as $row) {
449 1
            if ($curType != $row['TypeID']) {
450 1
                if ($curType > 0) {
451
                    $facets['ATT'.$curType] = $curFacet;
452
                }
453 1
                $curType = $row['TypeID'];
454
                $curFacet = array(
455 1
                    'Label'  => $row['TypeLabel'],
456 1
                    'Source' => 'ATT'.$curType,
457 1
                    'Type'   => ShopSearch::FACET_TYPE_LINK,
458 1
                    'Values' => array(),
459 1
                );
460 1
            }
461
462 1
            unset($row['TypeID']);
463 1
            unset($row['TypeLabel']);
464 1
            $curFacet['Values'][ $row['Value'] ] = new ArrayData($row);
465 1
        }
466
467 1
        if ($curType > 0) {
468 1
            $facets['ATT'.$curType] = $curFacet;
469 1
        }
470 1
        return $facets;
471
    }
472
473
474
    /**
475
     * Inserts a "Link" field into the values for each facet which can be
476
     * used to get a filtered search based on that facets
477
     *
478
     * @param ArrayList $facets
479
     * @param array     $baseParams
480
     * @param string    $baseLink
481
     * @return ArrayList
482
     */
483 1
    public function insertFacetLinks(ArrayList $facets, array $baseParams, $baseLink)
484
    {
485 1
        $qs_f   = Config::inst()->get('ShopSearch', 'qs_filters');
486 1
        $qs_t   = Config::inst()->get('ShopSearch', 'qs_title');
487
488 1
        foreach ($facets as $facet) {
489 1
            switch ($facet->Type) {
490 1
                case ShopSearch::FACET_TYPE_RANGE:
491 1
                    $params = array_merge($baseParams, array());
492 1
                    if (!isset($params[$qs_f])) {
493 1
                        $params[$qs_f] = array();
494 1
                    }
495 1
                    $params[$qs_f][$facet->Source] = 'RANGEFACETVALUE';
496 1
                    $params[$qs_t] = $facet->Label . ': RANGEFACETLABEL';
497 1
                    $facet->Link = $baseLink . '?' . http_build_query($params);
498 1
                break;
499
500 1
                case ShopSearch::FACET_TYPE_CHECKBOX;
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
501 1
                    $facet->LinkDetails = json_encode(array(
502 1
                        'filter'    => $qs_f,
503 1
                        'source'    => $facet->Source,
504 1
                        'leaves'    => $facet->FilterOnlyLeaves,
505 1
                    ));
506
507
                    // fall through on purpose
508
509 1
                default:
510 1
                    foreach ($facet->Values as $value) {
511
                        // make a copy of the existing params
512 1
                        $params = array_merge($baseParams, array());
513
514
                        // add the filter for this value
515 1
                        if (!isset($params[$qs_f])) {
516 1
                            $params[$qs_f] = array();
517 1
                        }
518 1
                        if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
519 1
                            unset($params[$qs_f][$facet->Source]); // this will be figured out via javascript
520 1
                            $params[$qs_t] = ($value->Active ? 'Remove ' : '') . $facet->Label . ': ' . $value->Label;
521 1
                        } else {
522
                            $params[$qs_f][$facet->Source] = $value->Value;
523
                            $params[$qs_t] = $facet->Label . ': ' . $value->Label;
524
                        }
525
526
                        // build a new link
527 1
                        $value->Link = $baseLink . '?' . http_build_query($params);
528 1
                    }
529 1
            }
530 1
        }
531
532 1
        return $facets;
533
    }
534
535
536
    /**
537
     * @param ArrayList $children
538
     * @return array
539
     */
540
    protected function getRecursiveChildValues(ArrayList $children)
541
    {
542
        $out = array();
543
544
        foreach ($children as $child) {
545
            $out[$child->Value] = $child->Value;
546
            if (!empty($child->Children)) {
547
                $out += $this->getRecursiveChildValues($child->Children);
548
            }
549
        }
550
551
        return $out;
552
    }
553
554
555
    /**
556
     * For checkbox and range facets, this updates the state (checked and min/max)
557
     * based on current filter values.
558
     *
559
     * @param ArrayList $facets
560
     * @param array     $filters
561
     * @return ArrayList
562
     */
563 1
    public function updateFacetState(ArrayList $facets, array $filters)
564
    {
565 1
        foreach ($facets as $facet) {
566 1
            if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
567 1
                if (empty($filters[$facet->Source])) {
568
                    // If the filter is not being used at all, we count
569
                    // all values as active.
570 1
                    foreach ($facet->Values as $value) {
571 1
                        $value->Active = (bool)FacetHelper::config()->default_checkbox_state;
572 1
                    }
573 1
                } else {
574
                    $filterVals = $filters[$facet->Source];
575
                    if (!is_array($filterVals)) {
576
                        $filterVals = array($filterVals);
577
                    }
578
                    $this->updateCheckboxFacetState(
579
                        !empty($facet->NestedValues) ? $facet->NestedValues : $facet->Values,
580
                        $filterVals,
581
                        !empty($facet->FilterOnlyLeaves));
582
                }
583 1
            } elseif ($facet->Type == ShopSearch::FACET_TYPE_RANGE) {
584 1
                if (!empty($filters[$facet->Source]) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filters[$facet->Source], $m)) {
585
                    $facet->MinValue = $m[1];
586
                    $facet->MaxValue = $m[2];
587
                }
588 1
            }
589 1
        }
590
591 1
        return $facets;
592
    }
593
594
595
    /**
596
     * For checkboxes, updates the state based on filters. Handles hierarchies and FilterOnlyLeaves
597
     * @param ArrayList $values
598
     * @param array     $filterVals
599
     * @param bool      $filterOnlyLeaves [optional]
600
     * @return bool - true if any of the children are true, false if all children are false
601
     */
602
    protected function updateCheckboxFacetState(ArrayList $values, array $filterVals, $filterOnlyLeaves=false)
603
    {
604
        $out = false;
605
606
        foreach ($values as $value) {
607
            if ($filterOnlyLeaves && !empty($value->Children)) {
608
                if (in_array($value->Value, $filterVals)) {
609
                    // This wouldn't be normal, but even if it's not a leaf, we want to handle
610
                    // the case where a filter might be set for this node. It should still show up correctly.
611
                    $value->Active = true;
612
                    foreach ($value->Children as $c) {
613
                        $c->Active = true;
614
                    }
615
                    // TODO: handle more than one level of recursion here
616
                } else {
617
                    $value->Active = $this->updateCheckboxFacetState($value->Children, $filterVals, $filterOnlyLeaves);
618
                }
619
            } else {
620
                $value->Active = in_array($value->Value, $filterVals);
621
            }
622
623
            if ($value->Active) {
624
                $out = true;
625
            }
626
        }
627
628
        return $out;
629
    }
630
631
632
    /**
633
     * If there are any facets (link or checkbox) that have a HierarchyDivider field
634
     * in the spec, transform them into a hierarchy so they can be displayed as such.
635
     *
636
     * @param ArrayList $facets
637
     * @return ArrayList
638
     */
639 1
    public function transformHierarchies(ArrayList $facets)
640
    {
641 1
        foreach ($facets as $facet) {
642 1
            if (!empty($facet->HierarchyDivider)) {
643
                $out = new ArrayList();
644
                $parentStack = array();
645
646
                foreach ($facet->Values as $value) {
647
                    if (empty($value->Label)) {
648
                        continue;
649
                    }
650
                    $value->FullLabel = $value->Label;
651
652
                    // Look for the most recent parent that matches the beginning of this one
653
                    while (count($parentStack) > 0) {
654
                        $curParent = $parentStack[ count($parentStack)-1 ];
655
                        if (strpos($value->Label, $curParent->FullLabel) === 0) {
656
                            if (!isset($curParent->Children)) {
657
                                $curParent->Children = new ArrayList();
658
                            }
659
660
                            // Modify the name so we only show the last component
661
                            $value->FullLabel = $value->Label;
662
                            $p = strrpos($value->Label, $facet->HierarchyDivider);
663
                            if ($p > -1) {
664
                                $value->Label = trim(substr($value->Label, $p + 1));
665
                            }
666
667
                            $curParent->Children->push($value);
668
                            break;
669
                        } else {
670
                            array_pop($parentStack);
671
                        }
672
                    }
673
674
                    // If we went all the way back to the root without a match, this is
675
                    // a new parent item
676
                    if (count($parentStack) == 0) {
677
                        $out->push($value);
678
                    }
679
680
                    // Each item could be a potential parent. If it's not it will get popped
681
                    // immediately on the next iteration
682
                    $parentStack[] = $value;
683
                }
684
685
                $facet->NestedValues = $out;
686
            }
687 1
        }
688
689 1
        return $facets;
690
    }
691
}
692