Completed
Push — master ( 553824...c19c0f )
by Nicolaas
02:44
created

ProductGroup_Controller::searchresults()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 16
nop 1
dl 0
loc 27
rs 9.1768
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Product Group is a 'holder' for Products within the CMS
5
 * It contains functions for versioning child products.
6
 *
7
 * The way the products are selected:
8
 *
9
 * Controller calls:
10
 * ProductGroup::ProductsShowable($extraFilter = "")
11
 *
12
 * ProductsShowable runs currentInitialProducts.  This selects ALL the applicable products
13
 * but it does NOT PAGINATE (limit) or SORT them.
14
 * After that, it calls currentFinalProducts, this sorts the products and notes the total
15
 * count of products (removing ones that can not be shown for one reason or another)
16
 *
17
 * Pagination is done in the controller.
18
 *
19
 * For each product page, there is a default:
20
 *  - filter
21
 *  - sort
22
 *  - number of levels to show (e.g. children, grand-children, etc...)
23
 * and these settings can be changed in the CMS, depending on what the
24
 * developer makes available to the content editor.
25
 *
26
 * In extending the ProductGroup class, it is recommended
27
 * that you override the following methods (as required ONLY!):
28
 * - getBuyableClassName
29
 * - getGroupFilter
30
 * - getStandardFilter
31
 * - getGroupJoin
32
 * - currentSortSQL
33
 * - limitCurrentFinalProducts
34
 * - removeExcludedProductsAndSaveIncludedProducts
35
 *
36
 * To filter products, you have three options:
37
 *
38
 * (1) getGroupFilter
39
 * - the standard product groups from which the products are selected
40
 * - if you extend Product Group this is the one you most likely want to change
41
 * - for example, rather than children, you set it to "yellow" products
42
 * - goes hand in hand with changes to showProductLevels / LevelOfProductsToShow
43
 * - works out the group filter based on the LevelOfProductsToShow value
44
 * - it also considers the other group many-many relationship
45
 * - this filter ALWAYS returns something: 1 = 1 if nothing else.
46
 *
47
 * (2) getStandardFilter
48
 * - these are the standard (user selectable) filters
49
 * - available options set via config
50
 * - the standard filter is updated by controller
51
 * - options can show above / below product lists to let user select alternative filter.
52
 *
53
 * (3) the extraWhere in ProductsShowable
54
 * - provided by the controller for specific ('on the fly') sub-sets
55
 * - this is for example for search results
56
 * - set in ProductShowable($extraWhere)
57
 *
58
 *
59
 * Caching
60
 * ==================
61
 *
62
 * There are two type of caching available:
63
 *
64
 * (1) caching of Product SQL queries
65
 *     - turned on and off by variable: ProductGroup->allowCaching
66
 *     - this is not a static so that you can create different settings for ProductGroup extensions.
67
 * (2) caching of product lists
68
 *     - see Product_Controller::ProductGroupListAreCacheable
69
 *
70
 * You can also ajaxify the product list, although this has nothing to do with
71
 * caching, it is related to it.
72
 *
73
 *
74
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
75
 * @package: ecommerce
76
 * @sub-package: Pages
77
 * @inspiration: Silverstripe Ltd, Jeremy
78
 **/
79
class ProductGroup extends Page
80
{
81
    /**
82
     * standard SS variable.
83
     *
84
     * @static Array
85
     */
86
    private static $db = array(
0 ignored issues
show
Unused Code introduced by
The property $db 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...
87
        'NumberOfProductsPerPage' => 'Int',
88
        'LevelOfProductsToShow' => 'Int',
89
        'DefaultSortOrder' => 'Varchar(20)',
90
        'DefaultFilter' => 'Varchar(20)',
91
        'DisplayStyle' => 'Varchar(20)',
92
    );
93
94
    /**
95
     * standard SS variable.
96
     *
97
     * @static Array
98
     */
99
    private static $has_one = array(
0 ignored issues
show
Unused Code introduced by
The property $has_one 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...
100
        'Image' => 'Product_Image',
101
    );
102
103
    /**
104
     * standard SS variable.
105
     *
106
     * @static Array
107
     */
108
    private static $belongs_many_many = array(
0 ignored issues
show
Unused Code introduced by
The property $belongs_many_many 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...
109
        'AlsoShowProducts' => 'Product',
110
    );
111
112
    /**
113
     * standard SS variable.
114
     *
115
     * @static Array
116
     */
117
    private static $defaults = array(
0 ignored issues
show
Unused Code introduced by
The property $defaults 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...
118
        'DefaultSortOrder' => 'default',
119
        'DefaultFilter' => 'default',
120
        'DisplayStyle' => 'default',
121
        'LevelOfProductsToShow' => 99,
122
    );
123
124
    /**
125
     * standard SS variable.
126
     *
127
     * @static Array
128
     */
129
    private static $indexes = array(
0 ignored issues
show
Unused Code introduced by
The property $indexes 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...
130
        'LevelOfProductsToShow' => true,
131
        'DefaultSortOrder' => true,
132
        'DefaultFilter' => true,
133
        'DisplayStyle' => true,
134
    );
135
136
    private static $summary_fields = array(
0 ignored issues
show
Unused Code introduced by
The property $summary_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...
137
        'Image.CMSThumbnail' => 'Image',
138
        'Title' => 'Category',
139
        'NumberOfProducts' => 'Direct Product Count'
140
    );
141
142
    private static $casting = array(
0 ignored issues
show
Unused Code introduced by
The property $casting 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...
143
        'NumberOfProducts' => 'Int'
144
    );
145
146
    /**
147
     * standard SS variable.
148
     *
149
     * @static String
150
     */
151
    private static $default_child = 'Product';
0 ignored issues
show
Unused Code introduced by
The property $default_child 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...
152
153
    /**
154
     * standard SS variable.
155
     *
156
     * @static String | Array
157
     */
158
    private static $icon = 'ecommerce/images/icons/productgroup';
0 ignored issues
show
Unused Code introduced by
The property $icon 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...
159
160
    /**
161
     * Standard SS variable.
162
     */
163
    private static $singular_name = 'Product Category';
0 ignored issues
show
Unused Code introduced by
The property $singular_name 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...
164
    public function i18n_singular_name()
165
    {
166
        return _t('ProductGroup.SINGULARNAME', 'Product Category');
167
    }
168
169
    /**
170
     * Standard SS variable.
171
     */
172
    private static $plural_name = 'Product Categories';
0 ignored issues
show
Unused Code introduced by
The property $plural_name 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...
173
    public function i18n_plural_name()
174
    {
175
        return _t('ProductGroup.PLURALNAME', 'Product Categories');
176
    }
177
178
    /**
179
     * Standard SS variable.
180
     *
181
     * @var string
182
     */
183
    private static $description = 'A page the shows a bunch of products, based on your selection. By default it shows products linked to it (children)';
0 ignored issues
show
Unused Code introduced by
The property $description 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...
184
185
    public function canCreate($member = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
186
    {
187
        if (! $member) {
188
            $member = Member::currentUser();
189
        }
190
        $extended = $this->extendedCan(__FUNCTION__, $member);
191
        if ($extended !== null) {
192
            return $extended;
193
        }
194
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
195
            return true;
196
        }
197
198
        return parent::canEdit($member);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (canEdit() instead of canCreate()). Are you sure this is correct? If so, you might want to change this to $this->canEdit().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
199
    }
200
201
    /**
202
     * Shop Admins can edit.
203
     *
204
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
205
     *
206
     * @return bool
207
     */
208
    public function canEdit($member = null)
209
    {
210
        if (! $member) {
211
            $member = Member::currentUser();
212
        }
213
        $extended = $this->extendedCan(__FUNCTION__, $member);
214
        if ($extended !== null) {
215
            return $extended;
216
        }
217
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
218
            return true;
219
        }
220
221
        return parent::canEdit($member);
222
    }
223
224
    /**
225
     * Standard SS method.
226
     *
227
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
228
     *
229
     * @return bool
230
     */
231
    public function canDelete($member = null)
232
    {
233
        if (is_a(Controller::curr(), Object::getCustomClass('ProductsAndGroupsModelAdmin'))) {
234
            return false;
235
        }
236
        if (! $member) {
237
            $member = Member::currentUser();
238
        }
239
        $extended = $this->extendedCan(__FUNCTION__, $member);
240
        if ($extended !== null) {
241
            return $extended;
242
        }
243
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
244
            return true;
245
        }
246
247
        return parent::canEdit($member);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (canEdit() instead of canDelete()). Are you sure this is correct? If so, you might want to change this to $this->canEdit().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
248
    }
249
250
    /**
251
     * Standard SS method.
252
     *
253
     * @param Member $member
0 ignored issues
show
Documentation introduced by
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
254
     *
255
     * @return bool
256
     */
257
    public function canPublish($member = null)
258
    {
259
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
260
            return true;
261
        }
262
263
        return parent::canEdit($member);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (canEdit() instead of canPublish()). Are you sure this is correct? If so, you might want to change this to $this->canEdit().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
264
    }
265
266
    /**
267
     * list of sort / filter / display variables.
268
     *
269
     * @var array
270
     */
271
    protected $sortFilterDisplayNames = array(
272
        'SORT' => array(
273
            'value' => 'default',
274
            'configName' => 'sort_options',
275
            'sessionName' => 'session_name_for_sort_preference',
276
            'getVariable' => 'sort',
277
            'dbFieldName' => 'DefaultSortOrder',
278
            'translationCode' => 'SORT_BY',
279
        ),
280
        'FILTER' => array(
281
            'value' => 'default',
282
            'configName' => 'filter_options',
283
            'sessionName' => 'session_name_for_filter_preference',
284
            'getVariable' => 'filter',
285
            'dbFieldName' => 'DefaultFilter',
286
            'translationCode' => 'FILTER_FOR',
287
        ),
288
        'DISPLAY' => array(
289
            'value' => 'default',
290
            'configName' => 'display_styles',
291
            'sessionName' => 'session_name_for_display_style_preference',
292
            'getVariable' => 'display',
293
            'dbFieldName' => 'DisplayStyle',
294
            'translationCode' => 'DISPLAY_STYLE',
295
        ),
296
    );
297
298
    /**
299
     * @var array
300
     *            List of options to show products.
301
     *            With it, we provide a bunch of methods to access and edit the options.
302
     *            NOTE: we can not have an option that has a zero key ( 0 => "none"), as this does not work
303
     *            (as it is equal to not completed yet - not yet entered in the Database).
304
     */
305
    protected $showProductLevels = array(
306
        99 => 'All Child Products (default)',
307
        -2 => 'None',
308
        -1 => 'All products',
309
        1 => 'Direct Child Products',
310
        2 => 'Direct Child Products + Grand Child Products',
311
        3 => 'Direct Child Products + Grand Child Products + Great Grand Child Products',
312
        4 => 'Direct Child Products + Grand Child Products + Great Grand Child Products + Great Great Grand Child Products',
313
    );
314
315
    /**
316
     * variable to speed up methods in this class.
317
     *
318
     * @var array
319
     */
320
    protected $configOptionsCache = array();
321
322
    /**
323
     * cache variable for default preference key.
324
     *
325
     * @var array
326
     */
327
    protected $myUserPreferencesDefaultCache = array();
328
329
    /**
330
     * count before limit.
331
     *
332
     * @var int
333
     */
334
    protected $rawCount = 0;
335
336
    /**
337
     * count after limit.
338
     *
339
     * @var int
340
     */
341
    protected $totalCount = 0;
342
343
    /**
344
     * Can product list (and related) be cached at all?
345
     * Set this to FALSE if the product details can be changed
346
     * for an individual user.
347
     *
348
     * @var bool
349
     */
350
    protected $allowCaching = true;
351
352
    /**
353
     * return the options for one type.
354
     * This method solely exists to speed up processing.
355
     *
356
     * @param string $type - options are FILTER | SORT | DISPLAY
357
     *
358
     * @return array
359
     */
360
    protected function getConfigOptions($type)
361
    {
362
        if (!isset($this->configOptionsCache[$type])) {
363
            $configName = $this->sortFilterDisplayNames[$type]['configName'];
364
            $this->configOptionsCache[$type] = EcommerceConfig::get($this->ClassName, $configName);
365
        }
366
367
        return $this->configOptionsCache[$type];
368
    }
369
370
    /**
371
     * returns the full sortFilterDisplayNames set, a subset, or one value
372
     * by either type (e.g. FILER) or variable (e.g dbFieldName)
373
     * or both.
374
     *
375
     * @param string $typeOfVariableName FILTER | SORT | DISPLAY or sessionName, getVariable, etc...
0 ignored issues
show
Documentation introduced by
There is no parameter named $typeOfVariableName. Did you maybe mean $variable?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
376
     * @param string $variable:          sessionName, getVariable, etc...
0 ignored issues
show
Documentation introduced by
There is no parameter named $variable:. Did you maybe mean $variable?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
377
     *
378
     * @return array | String
379
     */
380
    protected function getSortFilterDisplayNames($typeOrVariable = '', $variable = '')
381
    {
382
        //return a string ...
383
        if ($variable) {
384
            return $this->sortFilterDisplayNames[$typeOrVariable][$variable];
385
        }
386
        //return an array ...
387
        $data = array();
388
        if (isset($this->sortFilterDisplayNames[$typeOrVariable])) {
389
            $data = $this->sortFilterDisplayNames[$typeOrVariable];
390
        } elseif ($typeOrVariable) {
391
            foreach ($this->sortFilterDisplayNames as $group) {
392
                $data[] = $group[$typeOrVariable];
393
            }
394
        } else {
395
            $data = $this->sortFilterDisplayNames;
396
        }
397
398
        return $data;
399
    }
400
401
    /**
402
     * sets a user preference.  This is typically used by the controller
403
     * to set filter and sort.
404
     *
405
     * @param string $type  SORT | FILTER | DISPLAY
406
     * @param string $value
407
     */
408
    protected function setCurrentUserPreference($type, $value)
409
    {
410
        $this->sortFilterDisplayNames[$type]['value'] = $value;
411
    }
412
413
    /**
414
     * Get a user preference.
415
     * This value can be updated by the controller
416
     * For example, the filter can be changed, based on a session value.
417
     *
418
     * @param string $type SORT | FILTER | DISPLAY
419
     *
420
     * @return string
421
     */
422
    protected function getCurrentUserPreferences($type)
423
    {
424
        return $this->sortFilterDisplayNames[$type]['value'];
425
    }
426
427
    /*********************
428
     * SETTINGS: Default Key
429
     *********************/
430
431
    /**
432
     * Checks for the most applicable user preferences for this page:
433
     * 1. what is saved in Database for this page.
434
     * 2. what the parent product group has saved in the database
435
     * 3. what the standard default is.
436
     *
437
     * @param string $type - FILTER | SORT | DISPLAY
438
     *
439
     * @return string - returns the key
440
     */
441
    protected function getMyUserPreferencesDefault($type)
442
    {
443
        if (!isset($this->myUserPreferencesDefaultCache[$type]) || !$this->myUserPreferencesDefaultCache[$type]) {
444
            $options = $this->getConfigOptions($type);
445
            $dbVariableName = $this->sortFilterDisplayNames[$type]['dbFieldName'];
446
            $defaultOption = '';
447
            if ($defaultOption == 'inherit' && $parent = $this->ParentGroup()) {
448
                $defaultOption = $parent->getMyUserPreferencesDefault($type);
449
            } elseif ($this->$dbVariableName && array_key_exists($this->$dbVariableName, $options)) {
450
                $defaultOption = $this->$dbVariableName;
451
            }
452
            if (!$defaultOption) {
453
                if (isset($options['default'])) {
454
                    $defaultOption = 'default';
455
                } else {
456
                    user_error("It is recommended that you have a default (key) option for $type", E_USER_NOTICE);
457
                    $keys = array_keys($options);
458
                    $defaultOption = $keys[0];
459
                }
460
            }
461
            $this->myUserPreferencesDefaultCache[$type] = $defaultOption;
462
        }
463
464
        return $this->myUserPreferencesDefaultCache[$type];
465
    }
466
467
    /*********************
468
     * SETTINGS: Dropdowns
469
     *********************/
470
    /**
471
     * SORT:
472
     * returns an array of Key => Title for sort options.
473
     *
474
     * FILTER:
475
     * Returns options for the dropdown of filter options.
476
     *
477
     * DISPLAY:
478
     * Returns the options for product display styles.
479
     * In the configuration you can set which ones are available.
480
     * If one is available then you must make sure that the corresponding template is available.
481
     * For example, if the display style is
482
     * MyTemplate => "All Details"
483
     * Then you must make sure MyTemplate.ss exists.
484
     *
485
     * @param string $type - FILTER | SORT | DISPLAY
486
     *
487
     * @return array
488
     */
489
    protected function getUserPreferencesOptionsForDropdown($type)
490
    {
491
        $options = $this->getConfigOptions($type);
492
        $inheritTitle = _t('ProductGroup.INHERIT', 'Inherit');
493
        $array = array('inherit' => $inheritTitle);
494
        if (is_array($options) && count($options)) {
495
            foreach ($options as $key => $option) {
496
                if (is_array($option)) {
497
                    $array[$key] = $option['Title'];
498
                } else {
499
                    $array[$key] = $option;
500
                }
501
            }
502
        }
503
504
        return $array;
505
    }
506
507
    /*********************
508
     * SETTINGS
509
     *********************/
510
511
    /**
512
     * check if the key is valid.
513
     *
514
     * @param  string $type     e.g. SORT | FILTER
515
     * @param  string $key      e.g. best_match | price | lastest
516
     * @param  string $variable e.g. SQL | Title
517
518
     * @return string - empty if not found
519
     */
520
    protected function getBestKeyAndValidateKey($type, $key = '', $variable = '')
521
    {
522
        $options = $this->getConfigOptions($type);
523
        //check !!!
524
        if($key && isset($options[$key])) {
525
            //all good
526
        } else {
527
            //reset
528
            $key = $this->getMyUserPreferencesDefault($type);
529
            $myGetVariable = $this->getSortFilterDisplayNames($type, 'getVariable');
0 ignored issues
show
Unused Code introduced by
$myGetVariable is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
530
            //clear bogus value from session ...
531
            $sessionName = $this->getSortFilterDisplayNames($type, 'sessionName');
532
            Session::set('ProductGroup_'.$sessionName, '');
533
        }
534
        if($key) {
535
            if($variable) {
536
                return $options[$key][$variable];
537
            }
538
        }
539
540
        return $key;
541
    }
542
543
544
    /**
545
     * SORT:
546
     * Returns the sort sql for a particular sorting key.
547
     * If no key is provided then the default key will be returned.
548
     *
549
     * @param string $key
550
     *
551
     * @return array (e.g. Array(MyField => "ASC", "MyOtherField" => "DESC")
552
     *
553
     * FILTER:
554
     * Returns the sql associated with a filter option.
555
     *
556
     * @param string $type - FILTER | SORT | DISPLAY
557
     * @param string $key  - the options selected
558
     *
559
     * @return array | String (e.g. array("MyField" => 1, "MyOtherField" => 0)) OR STRING
0 ignored issues
show
Documentation introduced by
Should the return type not be string|array<string,stri...ay<string,integer>|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
560
     */
561
    protected function getUserSettingsOptionSQL($type, $key = '')
562
    {
563
        $value = $this->getBestKeyAndValidateKey($type, $key, 'SQL');
564
        if($value) {
565
            return $value;
566
        } else {
567
            if ($type == 'FILTER') {
568
                return array('Sort' => 'ASC');
569
            } elseif ($type == 'SORT') {
570
                return array('ShowInSearch' => 1);
571
            }
572
        }
573
    }
574
575
    /*********************
576
     * SETTINGS: Title
577
     *********************/
578
579
    /**
580
     * Returns the Title for a type key.
581
     * If no key is provided then the default key is used.
582
     *
583
     * @param string $type - FILTER | SORT | DISPLAY
584
     * @param string $key
585
     *
586
     * @return string
587
     */
588
    public function getUserPreferencesTitle($type, $key = '')
589
    {
590
        $value = $this->getBestKeyAndValidateKey($type, $key, 'Title');
591
        if($value) {
592
            return $value;
593
        } else {
594
            return _t('ProductGroup.UNKNOWN', 'UNKNOWN USER SETTING');
595
        }
596
    }
597
598
    /*********************
599
     * SETTINGS: products per page
600
     *********************/
601
602
    /**
603
     * @return int
604
     **/
605
    public function ProductsPerPage()
606
    {
607
        return $this->MyNumberOfProductsPerPage();
608
    }
609
610
    private $_numberOfProductsPerPage = null;
611
612
    /**
613
     * @return int
614
     **/
615
    public function MyNumberOfProductsPerPage()
616
    {
617
        if ($this->_numberOfProductsPerPage === null) {
618
            $productsPagePage = 0;
0 ignored issues
show
Unused Code introduced by
$productsPagePage is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
619
            if ($this->NumberOfProductsPerPage) {
620
                $productsPagePage = $this->NumberOfProductsPerPage;
621
            } else {
622
                if ($parent = $this->ParentGroup()) {
623
                    $productsPagePage = $parent->MyNumberOfProductsPerPage();
624
                } else {
625
                    $productsPagePage = $this->EcomConfig()->NumberOfProductsPerPage;
626
                }
627
            }
628
            $this->_numberOfProductsPerPage = $productsPagePage;
629
        }
630
        return $this->_numberOfProductsPerPage;
631
    }
632
633
    /*********************
634
     * SETTINGS: level of products to show
635
     *********************/
636
637
    /**
638
     * returns the number of product groups (children)
639
     * to show in the current product list
640
     * based on the user setting for this page.
641
     *
642
     * @return int
643
     */
644
    public function MyLevelOfProductsToShow()
645
    {
646
        if ($this->LevelOfProductsToShow == 0) {
647
            if ($parent = $this->ParentGroup()) {
648
                $this->LevelOfProductsToShow = $parent->MyLevelOfProductsToShow();
649
            }
650
        }
651
        //reset to default
652
        if ($this->LevelOfProductsToShow     == 0) {
653
            $defaults = Config::inst()->get('ProductGroup', 'defaults');
654
655
            return isset($defaults['LevelOfProductsToShow']) ? $defaults['LevelOfProductsToShow'] : 99;
656
        }
657
658
        return $this->LevelOfProductsToShow;
659
    }
660
661
    /*********************
662
     * CMS Fields
663
     *********************/
664
665
    /**
666
     * standard SS method.
667
     *
668
     * @return FieldList
669
     */
670
    public function getCMSFields()
671
    {
672
        $fields = parent::getCMSFields();
673
        //dirty hack to show images!
674
        $fields->addFieldToTab('Root.Images', Product_ProductImageUploadField::create('Image', _t('Product.IMAGE', 'Product Group Image')));
675
        //number of products
676
        $calculatedNumberOfProductsPerPage = $this->MyNumberOfProductsPerPage();
677
        $numberOfProductsPerPageExplanation = $calculatedNumberOfProductsPerPage != $this->NumberOfProductsPerPage ? _t('ProductGroup.CURRENTLVALUE', 'Current value: ').$calculatedNumberOfProductsPerPage.' '._t('ProductGroup.INHERITEDFROMPARENTSPAGE', ' (inherited from parent page because the current page is set to zero)') : '';
678
        $fields->addFieldToTab(
679
            'Root',
680
            Tab::create(
681
                'ProductDisplay',
682
                _t('ProductGroup.DISPLAY', 'Display'),
683
                $productsToShowField = DropdownField::create('LevelOfProductsToShow', _t('ProductGroup.PRODUCTSTOSHOW', 'Products to show'), $this->showProductLevels),
684
                HeaderField::create('WhatProductsAreShown', _t('ProductGroup.WHATPRODUCTSSHOWN', _t('ProductGroup.OPTIONSSELECTEDBELOWAPPLYTOCHILDGROUPS', 'Inherited options'))),
685
                $numberOfProductsPerPageField = NumericField::create('NumberOfProductsPerPage', _t('ProductGroup.PRODUCTSPERPAGE', 'Number of products per page'))
686
            )
687
        );
688
        $numberOfProductsPerPageField->setRightTitle($numberOfProductsPerPageExplanation);
689
        if ($calculatedNumberOfProductsPerPage && !$this->NumberOfProductsPerPage) {
690
            $this->NumberOfProductsPerPage = null;
691
            $numberOfProductsPerPageField->setAttribute('placeholder', $calculatedNumberOfProductsPerPage);
692
        }
693
        //sort
694
        $sortDropdownList = $this->getUserPreferencesOptionsForDropdown('SORT');
695
        if (count($sortDropdownList) > 1) {
696
            $sortOrderKey = $this->getMyUserPreferencesDefault('SORT');
697
            if ($this->DefaultSortOrder == 'inherit') {
698
                $actualValue = ' ('.(isset($sortDropdownList[$sortOrderKey]) ? $sortDropdownList[$sortOrderKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
699
                $sortDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
700
            }
701
            $fields->addFieldToTab(
702
                'Root.ProductDisplay',
703
                $defaultSortOrderField = DropdownField::create('DefaultSortOrder', _t('ProductGroup.DEFAULTSORTORDER', 'Default Sort Order'), $sortDropdownList)
704
            );
705
            $defaultSortOrderField->setRightTitle(_t('ProductGroup.INHERIT_RIGHT_TITLE', "Inherit means that the parent page value is used - and if there is no relevant parent page then the site's default value is used."));
706
        }
707
        //filter
708
        $filterDropdownList = $this->getUserPreferencesOptionsForDropdown('FILTER');
709
        if (count($filterDropdownList) > 1) {
710
            $filterKey = $this->getMyUserPreferencesDefault('FILTER');
711
            if ($this->DefaultFilter == 'inherit') {
712
                $actualValue = ' ('.(isset($filterDropdownList[$filterKey]) ? $filterDropdownList[$filterKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
713
                $filterDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
714
            }
715
            $fields->addFieldToTab(
716
                'Root.ProductDisplay',
717
                $defaultFilterField = DropdownField::create('DefaultFilter', _t('ProductGroup.DEFAULTFILTER', 'Default Filter'), $filterDropdownList)
718
            );
719
            $defaultFilterField->setRightTitle(_t('ProductGroup.INHERIT_RIGHT_TITLE', "Inherit means that the parent page value is used - and if there is no relevant parent page then the site's default value is used."));
720
        }
721
        //display style
722
        $displayStyleDropdownList = $this->getUserPreferencesOptionsForDropdown('DISPLAY');
723
        if (count($displayStyleDropdownList) > 2) {
724
            $displayStyleKey = $this->getMyUserPreferencesDefault('DISPLAY');
725
            if ($this->DisplayStyle == 'inherit') {
726
                $actualValue = ' ('.(isset($displayStyleDropdownList[$displayStyleKey]) ? $displayStyleDropdownList[$displayStyleKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
727
                $displayStyleDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
728
            }
729
            $fields->addFieldToTab(
730
                'Root.ProductDisplay',
731
                DropdownField::create('DisplayStyle', _t('ProductGroup.DEFAULTDISPLAYSTYLE', 'Default Display Style'), $displayStyleDropdownList)
732
            );
733
        }
734
        if ($this->EcomConfig()->ProductsAlsoInOtherGroups) {
735
            if (!$this instanceof ProductGroupSearchPage) {
736
                $fields->addFieldsToTab(
737
                    'Root.OtherProductsShown',
738
                    array(
739
                        HeaderField::create('ProductGroupsHeader', _t('ProductGroup.OTHERPRODUCTSTOSHOW', 'Other products to show ...')),
740
                        $this->getProductGroupsTable(),
741
                    )
742
                );
743
            }
744
        }
745
746
        return $fields;
747
    }
748
749
    /**
750
     * used if you install lumberjack
751
     * @return string
752
     */
753
    public function getLumberjackTitle()
754
    {
755
        return _t('ProductGroup.BUYABLES', 'Products');
756
    }
757
758
    /**
759
     * add this segment to the end of a Product Group
760
     * link to create a cross-filter between the two categories.
761
     *
762
     * @return string
763
     */
764
    public function FilterForGroupLinkSegment()
765
    {
766
        return 'filterforgroup/'.$this->URLSegment.'/';
767
    }
768
769
    // /**
770
    //  * used if you install lumberjack
771
    //  * @return string
772
    //  */
773
    // public function getLumberjackGridFieldConfig()
774
    // {
775
    //     return GridFieldConfig_RelationEditor::create();
776
    // }
777
778
    /**
779
     * Used in getCSMFields.
780
     *
781
     * @return GridField
782
     **/
783
    protected function getProductGroupsTable()
784
    {
785
        $gridField = GridField::create(
786
            'AlsoShowProducts',
787
            _t('ProductGroup.OTHER_PRODUCTS_SHOWN_IN_THIS_GROUP', 'Other products shown in this group ...'),
788
            $this->AlsoShowProducts(),
789
            GridFieldBasicPageRelationConfig::create()
790
        );
791
        //make sure edits are done in the right place ...
792
        return $gridField;
793
    }
794
795
    /*****************************************************
796
     *
797
     *
798
     *
799
     * PRODUCTS THAT BELONG WITH THIS PRODUCT GROUP
800
     *
801
     *
802
     *
803
     *****************************************************/
804
805
    /**
806
     * returns the inital (all) products, based on the all the eligible products
807
     * for the page.
808
     *
809
     * This is THE pivotal method that probably changes for classes that
810
     * extend ProductGroup as here you can determine what products or other buyables are shown.
811
     *
812
     * The return from this method will then be sorted to produce the final product list.
813
     *
814
     * There is no sort for the initial retrieval
815
     *
816
     * This method is public so that you can retrieve a list of products for a product group page.
817
     *
818
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
819
     * @param string         $alternativeFilterKey Alternative standard filter to be used.
820
     *
821
     * @return DataList
822
     **/
823
    public function currentInitialProducts($extraFilter = null, $alternativeFilterKey = '')
824
    {
825
826
        //INIT ALLPRODUCTS
827
        $this->setProductBase();
828
829
        // GROUP FILTER (PRODUCTS FOR THIS GROUP)
830
        $this->allProducts = $this->getGroupFilter();
831
832
        // STANDARD FILTER (INCLUDES USER PREFERENCE)
833
        $filterStatement = $this->allowPurchaseWhereStatement();
834
        if ($filterStatement) {
835
            if (is_array($filterStatement)) {
836
                $this->allProducts = $this->allProducts->filter($filterStatement);
837
            } elseif (is_string($filterStatement)) {
838
                $this->allProducts = $this->allProducts->where($filterStatement);
839
            }
840
        }
841
        $this->allProducts = $this->getStandardFilter($alternativeFilterKey);
842
843
        // EXTRA FILTER (ON THE FLY FROM CONTROLLER)
844
        if (is_array($extraFilter) && count($extraFilter)) {
845
            $this->allProducts = $this->allProducts->filter($extraFilter);
846
        } elseif (is_string($extraFilter) && strlen($extraFilter) > 2) {
847
            $this->allProducts = $this->allProducts->where($extraFilter);
848
        }
849
850
        //JOINS
851
        $this->allProducts = $this->getGroupJoin();
852
853
        return $this->allProducts;
854
    }
855
856
    protected function setProductBase()
857
    {
858
        unset($this->allProducts);
859
        $className = $this->getBuyableClassName();
860
        $this->allProducts = $className::get();
861
    }
862
863
    /**
864
     * this method can be used quickly current initial products
865
     * whenever you write:
866
     *  ```php
867
     *   currentInitialProducts->(null, $key)->map("ID", "ID")->toArray();
868
     *  ```
869
     * this is the better replacement.
870
     *
871
     * @param string $filterKey
872
     *
873
     * @return array
874
     */
875
    public function currentInitialProductsAsCachedArray($filterKey)
876
    {
877
        $cacheKey = 'CurrentInitialProductsArray'.$filterKey;
878
        if ($array = $this->retrieveObjectStore($cacheKey)) {
879
            //do nothing
880
        } else {
881
            $array = $this->currentInitialProducts(null, $filterKey)->map('ID', 'ID')->toArray();
882
            $this->saveObjectStore($array, $cacheKey);
883
        }
884
885
        return $array;
886
    }
887
888
    /*****************************************************
889
     * DATALIST: adjusters
890
     * these are the methods you want to override in
891
     * any clases that extend ProductGroup
892
     *****************************************************/
893
894
    /**
895
     * Do products occur in more than one group.
896
     *
897
     * @return bool
898
     */
899
    protected function getProductsAlsoInOtherGroups()
900
    {
901
        return $this->EcomConfig()->ProductsAlsoInOtherGroups;
902
    }
903
904
    /**
905
     * Returns the class we are working with.
906
     *
907
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be array|integer|double|string|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
908
     */
909
    protected function getBuyableClassName()
910
    {
911
        return EcommerceConfig::get('ProductGroup', 'base_buyable_class');
912
    }
913
914
    /**
915
     * @SEE: important notes at the top of this file / class
916
     *
917
     * IMPORTANT: Adjusts allProducts and returns it...
918
     *
919
     * @return DataList
920
     */
921
    protected function getGroupFilter()
922
    {
923
        $levelToShow = $this->MyLevelOfProductsToShow();
924
        $cacheKey = 'GroupFilter_'.abs(intval($levelToShow + 999));
925
        if ($groupFilter = $this->retrieveObjectStore($cacheKey)) {
926
            $this->allProducts = $this->allProducts->where($groupFilter);
927
        } else {
928
            $groupFilter = '';
0 ignored issues
show
Unused Code introduced by
$groupFilter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
929
            $productFilterArray = array();
930
            //special cases
931
            if ($levelToShow < 0) {
932
                //no produts but if LevelOfProductsToShow = -1 then show all
933
                $groupFilter = ' ('.$levelToShow.' = -1) ';
934
            } elseif ($levelToShow > 0) {
935
                $groupIDs = array($this->ID => $this->ID);
936
                $productFilterTemp = $this->getProductsToBeIncludedFromOtherGroups();
937
                $productFilterArray[$productFilterTemp] = $productFilterTemp;
938
                $childGroups = $this->ChildGroups($levelToShow);
939
                if ($childGroups && $childGroups->count()) {
940
                    foreach ($childGroups as $childGroup) {
941
                        $groupIDs[$childGroup->ID] = $childGroup->ID;
942
                        $productFilterTemp = $childGroup->getProductsToBeIncludedFromOtherGroups();
943
                        $productFilterArray[$productFilterTemp] = $productFilterTemp;
944
                    }
945
                }
946
                $groupFilter = ' ( "ParentID" IN ('.implode(',', $groupIDs).') ) '.implode($productFilterArray).' ';
947
            } else {
948
                //fall-back
949
                $groupFilter = '"ParentID" < 0';
950
            }
951
            $this->allProducts = $this->allProducts->where($groupFilter);
952
            $this->saveObjectStore($groupFilter, $cacheKey);
953
        }
954
955
        return $this->allProducts;
956
    }
957
958
    /**
959
     * If products are show in more than one group
960
     * Then this returns a where phrase for any products that are linked to this
961
     * product group.
962
     *
963
     * @return string
964
     */
965
    protected function getProductsToBeIncludedFromOtherGroups()
966
    {
967
        //TO DO: this should actually return
968
        //Product.ID = IN ARRAY(bla bla)
969
        $array = array();
970
        if ($this->getProductsAlsoInOtherGroups()) {
971
            $array = $this->AlsoShowProducts()->map('ID', 'ID')->toArray();
972
        }
973
        if (count($array)) {
974
            return " OR (\"Product\".\"ID\" IN (".implode(',', $array).')) ';
975
        }
976
977
        return '';
978
    }
979
980
    /**
981
     * @SEE: important notes at the top of this class / file for more information!
982
     *
983
     * IMPORTANT: Adjusts allProducts and returns it...
984
     *
985
     * @param string $alternativeFilterKey - filter key to be used... if none is specified then we use the current one.
986
     *
987
     * @return DataList
988
     */
989
    protected function getStandardFilter($alternativeFilterKey = '')
990
    {
991
        if ($alternativeFilterKey) {
992
            $filterKey = $alternativeFilterKey;
993
        } else {
994
            $filterKey = $this->getCurrentUserPreferences('FILTER');
995
        }
996
        $filter = $this->getUserSettingsOptionSQL('FILTER', $filterKey);
997
        if (is_array($filter)) {
998
            $this->allProducts = $this->allProducts->Filter($filter);
999
        } elseif (is_string($filter) && strlen($filter) > 2) {
1000
            $this->allProducts = $this->allProducts->Where($filter);
1001
        }
1002
1003
        return $this->allProducts;
1004
    }
1005
1006
    /**
1007
     * Join statement for the product groups.
1008
     *
1009
     * IMPORTANT: Adjusts allProducts and returns it...
1010
     *
1011
     * @return DataList
1012
     */
1013
    protected function getGroupJoin()
1014
    {
1015
        return $this->allProducts;
1016
    }
1017
1018
    /**
1019
     * Quick - dirty hack - filter to
1020
     * only show relevant products.
1021
     *
1022
     * @param bool   $asArray
1023
     * @param string $table
1024
     */
1025
    protected function allowPurchaseWhereStatement($asArray = true, $table = 'Product')
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1026
    {
1027
        if ($this->EcomConfig()->OnlyShowProductsThatCanBePurchased) {
1028
            if ($asArray) {
1029
                $allowPurchaseWhereStatement = array('AllowPurchase' => 1);
1030
            } else {
1031
                $allowPurchaseWhereStatement = "\"$table\".\"AllowPurchase\" = 1  ";
1032
            }
1033
1034
            return $allowPurchaseWhereStatement;
1035
        }
1036
    }
1037
1038
    /*****************************************************
1039
     *
1040
     *
1041
     *
1042
     *
1043
     * FINAL PRODUCTS
1044
     *
1045
     *
1046
     *
1047
     *
1048
     *****************************************************/
1049
1050
    /**
1051
     * This is the dataList that contains all the products.
1052
     *
1053
     * @var DataList
1054
     */
1055
    protected $allProducts = null;
1056
1057
    /**
1058
     * a list of relevant buyables that can
1059
     * not be purchased and therefore should be excluded.
1060
     * Should be set to NULL to start with so we know if it has been
1061
     * set yet.
1062
     *
1063
     * @var null | Array (like so: array(1,2,4,5,99))
1064
     */
1065
    private $canNOTbePurchasedArray = null;
1066
1067
    /**
1068
     * a list of relevant buyables that can
1069
     * be purchased.  We keep this so that
1070
     * that we can save to session, etc... for future use.
1071
     * Should be set to NULL to start with so we know if it has been
1072
     * set yet.
1073
     *
1074
     * @var null | Array (like so: array(1,2,4,5,99))
1075
     */
1076
    protected $canBePurchasedArray = null;
1077
1078
    /**
1079
     * returns the total numer of products
1080
     * (before pagination AND before MAX is applie).
1081
     *
1082
     * @return int
1083
     **/
1084
    public function RawCount()
1085
    {
1086
        return $this->rawCount ? $this->rawCount : 0;
1087
    }
1088
1089
    /**
1090
     * returns the total numer of products
1091
     * (before pagination but after MAX is applied).
1092
     *
1093
     * @return int
1094
     **/
1095
    public function TotalCount()
1096
    {
1097
        return $this->totalCount ? $this->totalCount : 0;
1098
    }
1099
1100
    /**
1101
     * this is used to save a list of sorted products
1102
     * so that you can find a previous and a next button, etc...
1103
     *
1104
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be array|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1105
     */
1106
    public function getProductsThatCanBePurchasedArray()
1107
    {
1108
        return $this->canBePurchasedArray;
1109
    }
1110
1111
    /**
1112
     * Retrieve a set of products, based on the given parameters.
1113
     * This method is usually called by the various controller methods.
1114
     * The extraFilter helps you to select different products,
1115
     * depending on the method used in the controller.
1116
     *
1117
     * Furthermore, extrafilter can take all sorts of variables.
1118
     * This is basically setup like this so that in ProductGroup extensions you
1119
     * can setup all sorts of filters, while still using the ProductsShowable method.
1120
     *
1121
     * The extra filter can be supplied as array (e.g. array("ID" => 12) or array("ID" => array(12,13,45)))
1122
     * or as string. Arrays are used like this $productDataList->filter($array) and
1123
     * strings are used with the where commands $productDataList->where($string).
1124
     *
1125
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
1126
     * @param array | string $alternativeSort      Additional SQL for sorting
1127
     * @param string         $alternativeFilterKey alternative filter key to be used
1128
     *
1129
     * @return DataList | Null
0 ignored issues
show
Documentation introduced by
Should the return type not be DataList|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1130
     */
1131
    public function ProductsShowable($extraFilter = null, $alternativeSort = null, $alternativeFilterKey = '')
1132
    {
1133
1134
        //get original products without sort
1135
        $this->allProducts = $this->currentInitialProducts($extraFilter, $alternativeFilterKey);
1136
1137
        //sort products
1138
        $this->allProducts = $this->currentFinalProducts($alternativeSort);
1139
1140
        return $this->allProducts;
1141
    }
1142
1143
    /**
1144
     * returns the final products, based on the all the eligile products
1145
     * for the page.
1146
     *
1147
     * In the process we also save a list of included products
1148
     * and we sort them.  We also keep a record of the total count.
1149
     *
1150
     * All of the 'current' methods are to support the currentFinalProducts Method.
1151
     *
1152
     * @TODO: cache data for faster access.
1153
     *
1154
     * @param array | string $alternativeSort = Alternative Sort String or array
1155
     *
1156
     * @return DataList
0 ignored issues
show
Documentation introduced by
Should the return type not be DataList|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1157
     **/
1158
    protected function currentFinalProducts($alternativeSort = null)
1159
    {
1160
        if ($this->allProducts) {
1161
            //limit to maximum number of products for speed's sake
1162
            $this->allProducts = $this->sortCurrentFinalProducts($alternativeSort);
1163
            $this->allProducts = $this->limitCurrentFinalProducts();
1164
            $this->allProducts = $this->removeExcludedProductsAndSaveIncludedProducts($this->allProducts);
1165
1166
            return $this->allProducts;
1167
        }
1168
    }
1169
1170
    /**
1171
     * returns the SORT part of the final selection of products.
1172
     *
1173
     * @return DataList (allProducts)
1174
     */
1175
    protected function sortCurrentFinalProducts($alternativeSort)
1176
    {
1177
        if ($alternativeSort) {
1178
            if ($this->IsIDarray($alternativeSort)) {
1179
                $sort = $this->createSortStatementFromIDArray($alternativeSort);
1180
            } else {
1181
                $sort = $alternativeSort;
1182
            }
1183
        } else {
1184
            $sort = $this->currentSortSQL();
1185
        }
1186
        $this->allProducts = $this->allProducts->Sort($sort);
1187
1188
        return $this->allProducts;
1189
    }
1190
1191
    /**
1192
     * is the variable provided is an array
1193
     * that can be used as a list of IDs?
1194
     *
1195
     * @param mixed
1196
     *
1197
     * @return bool
1198
     */
1199
    protected function IsIDarray($variable)
1200
    {
1201
        return $variable && is_array($variable) && count($variable) && intval(current($variable)) == current($variable);
1202
    }
1203
1204
    /**
1205
     * returns the SORT part of the final selection of products.
1206
     *
1207
     * @return string | Array
0 ignored issues
show
Documentation introduced by
Should the return type not be string|array<string,stri...ay<string,integer>|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1208
     */
1209
    protected function currentSortSQL()
1210
    {
1211
        $sortKey = $this->getCurrentUserPreferences('SORT');
1212
1213
        return $this->getUserSettingsOptionSQL('SORT', $sortKey);
1214
    }
1215
1216
    /**
1217
     * creates a sort string from a list of ID arrays...
1218
     *
1219
     * @param array $IDarray - list of product IDs
1220
     *
1221
     * @return string
1222
     */
1223
    protected function createSortStatementFromIDArray($IDarray, $table = 'Product')
1224
    {
1225
        $ifStatement = 'CASE ';
1226
        $sortStatement = '';
0 ignored issues
show
Unused Code introduced by
$sortStatement is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1227
        $stage = $this->getStage();
1228
        $count = 0;
1229
        foreach ($IDarray as $productID) {
1230
            $ifStatement .= ' WHEN "'.$table.$stage."\".\"ID\" = $productID THEN $count";
1231
            ++$count;
1232
        }
1233
        $sortStatement = $ifStatement.' END';
1234
1235
        return $sortStatement;
1236
    }
1237
1238
    /**
1239
     * limits the products to a maximum number (for speed's sake).
1240
     *
1241
     * @return DataList (this->allProducts adjusted!)
1242
     */
1243
    protected function limitCurrentFinalProducts()
1244
    {
1245
        $this->rawCount = $this->allProducts->count();
1246
        $max = EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list');
1247
        if ($this->rawCount > $max) {
1248
            $this->allProducts = $this->allProducts->limit($max);
1249
            $this->totalCount = $max;
1250
        } else {
1251
            $this->totalCount = $this->rawCount;
1252
        }
1253
1254
        return $this->allProducts;
1255
    }
1256
1257
    /**
1258
     * Excluded products that can not be purchased
1259
     * We all make a record of all the products that are in the current list
1260
     * For efficiency sake, we do both these things at the same time.
1261
     * IMPORTANT: Adjusts allProducts and returns it...
1262
     *
1263
     * @todo: cache data per user ....
1264
     *
1265
     * @return DataList
1266
     */
1267
    protected function removeExcludedProductsAndSaveIncludedProducts()
1268
    {
1269
        if (is_array($this->canBePurchasedArray) && is_array($this->canNOTbePurchasedArray)) {
1270
            //already done!
1271
        } else {
1272
            $this->canNOTbePurchasedArray = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $canNOTbePurchasedArray.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1273
            $this->canBePurchasedArray = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type null of property $canBePurchasedArray.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1274
            if ($this->config()->get('actively_check_for_can_purchase')) {
1275
                foreach ($this->allProducts as $buyable) {
1276
                    if ($buyable->canPurchase()) {
1277
                        $this->canBePurchasedArray[$buyable->ID] = $buyable->ID;
1278
                    } else {
1279
                        $this->canNOTbePurchasedArray[$buyable->ID] = $buyable->ID;
1280
                    }
1281
                }
1282
            } else {
1283
                if ($this->rawCount > 0) {
1284
                    $this->canBePurchasedArray = $this->allProducts->map('ID', 'ID')->toArray();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->allProducts->map('ID', 'ID')->toArray() of type array is incompatible with the declared type null of property $canBePurchasedArray.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1285
                } else {
1286
                    $this->canBePurchasedArray = array();
1287
                }
1288
            }
1289
            if (count($this->canNOTbePurchasedArray)) {
1290
                $this->allProducts = $this->allProducts->exclude(array('ID' => $this->canNOTbePurchasedArray));
1291
            }
1292
        }
1293
1294
        return $this->allProducts;
1295
    }
1296
1297
    /*****************************************************
1298
     * Children and Parents
1299
     *****************************************************/
1300
1301
    /**
1302
     * Returns children ProductGroup pages of this group.
1303
     *
1304
     * @param int            $maxRecursiveLevel  - maximum depth , e.g. 1 = one level down - so no Child Groups are returned...
1305
     * @param string | Array $filter             - additional filter to be added
1306
     * @param int            $numberOfRecursions - current level of depth
1307
     *
1308
     * @return ArrayList (ProductGroups)
1309
     */
1310
    public function ChildGroups($maxRecursiveLevel, $filter = null, $numberOfRecursions = 0)
1311
    {
1312
        $arrayList = ArrayList::create();
1313
        ++$numberOfRecursions;
1314
        if ($numberOfRecursions < $maxRecursiveLevel) {
1315
            if ($filter && is_string($filter)) {
1316
                $filterWithAND = " AND $filter";
1317
                $where = "\"ParentID\" = '$this->ID' $filterWithAND";
1318
                $children = ProductGroup::get()->where($where);
1319
            } elseif (is_array($filter) && count($filter)) {
1320
                $filter = $filter + array('ParentID' => $this->ID);
1321
                $children = ProductGroup::get()->filter($filter);
1322
            } else {
1323
                $children = ProductGroup::get()->filter(array('ParentID' => $this->ID));
1324
            }
1325
1326
            if ($children->count()) {
1327
                foreach ($children as $child) {
1328
                    $arrayList->push($child);
1329
                    $arrayList->merge($child->ChildGroups($maxRecursiveLevel, $filter, $numberOfRecursions));
1330
                }
1331
            }
1332
        }
1333
        if (! ($arrayList instanceof SS_List)) {
1334
            user_error('We expect an SS_List as output');
1335
        }
1336
1337
        return $arrayList;
1338
    }
1339
1340
    /**
1341
     * Deprecated method.
1342
     */
1343
    public function ChildGroupsBackup($maxRecursiveLevel, $filter = '')
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1344
    {
1345
        Deprecation::notice('3.1', 'No longer in use');
1346
        if ($maxRecursiveLevel > 24) {
1347
            $maxRecursiveLevel = 24;
1348
        }
1349
1350
        $stage = $this->getStage();
1351
        $select = 'P1.ID as ID1 ';
1352
        $from = "ProductGroup$stage as P1 ";
1353
        $join = " INNER JOIN SiteTree$stage AS S1 ON P1.ID = S1.ID";
1354
        $where = '1 = 1';
1355
        $ids = array(-1);
1356
        for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1357
            $j = $i + 1;
1358
            $select .= ", P$j.ID AS ID$j, S$j.ParentID";
1359
            $join .= "
1360
                LEFT JOIN ProductGroup$stage AS P$j ON P$j.ID = S$i.ParentID
1361
                LEFT JOIN SiteTree$stage AS S$j ON P$j.ID = S$j.ID
1362
            ";
1363
        }
1364
        $rows = DB::Query(' SELECT '.$select.' FROM '.$from.$join.' WHERE '.$where);
1365
        if ($rows) {
1366
            foreach ($rows as $row) {
1367
                for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1368
                    if ($row['ID'.$i]) {
1369
                        $ids[$row['ID'.$i]] = $row['ID'.$i];
1370
                    }
1371
                }
1372
            }
1373
        }
1374
1375
        return ProductGroup::get()->where("\"ProductGroup$stage\".\"ID\" IN (".implode(',', $ids).')'.$filterWithAND);
0 ignored issues
show
Bug introduced by
The variable $filterWithAND does not exist. Did you mean $filter?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1376
    }
1377
1378
    /**
1379
     * returns the parent page, but only if it is an instance of Product Group.
1380
     *
1381
     * @return DataObject | Null (ProductGroup)
1382
     **/
1383
    public function ParentGroup()
1384
    {
1385
        if ($this->ParentID) {
1386
            return ProductGroup::get()->byID($this->ParentID);
1387
        }
1388
    }
1389
1390
    /*****************************************************
1391
     * Other Stuff
1392
     *****************************************************/
1393
1394
    /**
1395
     * Recursively generate a product menu.
1396
     *
1397
     * @param string $filter
1398
     *
1399
     * @return ArrayList (ProductGroups)
1400
     */
1401
    public function GroupsMenu($filter = 'ShowInMenus = 1')
1402
    {
1403
        if ($parent = $this->ParentGroup()) {
1404
            return is_a($parent, Object::getCustomClass('ProductGroup')) ? $parent->GroupsMenu() : $this->ChildGroups($filter);
1405
        } else {
1406
            return $this->ChildGroups($filter);
1407
        }
1408
    }
1409
1410
    /**
1411
     * returns a "BestAvailable" image if the current one is not available
1412
     * In some cases this is appropriate and in some cases this is not.
1413
     * For example, consider the following setup
1414
     * - product A with three variations
1415
     * - Product A has an image, but the variations have no images
1416
     * With this scenario, you want to show ONLY the product image
1417
     * on the product page, but if one of the variations is added to the
1418
     * cart, then you want to show the product image.
1419
     * This can be achieved bu using the BestAvailable image.
1420
     *
1421
     * @return Image | Null
1422
     */
1423
    public function BestAvailableImage()
1424
    {
1425
        $image = $this->Image();
1426
        if ($image && $image->exists() && file_exists($image->getFullPath())) {
1427
            return $image;
1428
        } elseif ($parent = $this->ParentGroup()) {
1429
            return $parent->BestAvailableImage();
1430
        }
1431
    }
1432
1433
    /*****************************************************
1434
     * Other related products
1435
     *****************************************************/
1436
1437
    /**
1438
     * returns a list of Product Groups that have the products for
1439
     * the CURRENT product group listed as part of their AlsoShowProducts list.
1440
     *
1441
     * EXAMPLE:
1442
     * You can use the AlsoShowProducts to list products by Brand.
1443
     * In general, they are listed under type product groups (e.g. socks, sweaters, t-shirts),
1444
     * and you create a list of separate ProductGroups (brands) that do not have ANY products as children,
1445
     * but link to products using the AlsoShowProducts many_many relation.
1446
     *
1447
     * With the method below you can work out a list of brands that apply to the
1448
     * current product group (e.g. socks come in three brands - namely A, B and C)
1449
     *
1450
     * @return DataList
1451
     */
1452
    public function ProductGroupsFromAlsoShowProducts()
1453
    {
1454
        $parentIDs = array();
1455
        //we need to add the last array to make sure we have some products...
1456
        $myProductsArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1457
        $rows = array();
1458
        if (count($myProductsArray)) {
1459
            $rows = DB::query('
1460
                SELECT "ProductGroupID"
1461
                FROM "Product_ProductGroups"
1462
                WHERE "ProductID" IN ('.implode(',', $myProductsArray).')
1463
                GROUP BY "ProductGroupID";
1464
            ');
1465
        }
1466
        foreach ($rows as $row) {
1467
            $parentIDs[$row['ProductGroupID']] = $row['ProductGroupID'];
1468
        }
1469
        //just in case
1470
        unset($parentIDs[$this->ID]);
1471
        if (!count($parentIDs)) {
1472
            $parentIDs = array(0 => 0);
1473
        }
1474
1475
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1476
    }
1477
1478
    /**
1479
     * This is the inverse of ProductGroupsFromAlsoShowProducts
1480
     * That is, it list the product groups that a product is primarily listed under (exact parents only)
1481
     * from a "AlsoShow" product List.
1482
     *
1483
     * @return DataList
1484
     */
1485
    public function ProductGroupsFromAlsoShowProductsInverse()
1486
    {
1487
        $alsoShowProductsArray = $this->AlsoShowProducts()
1488
            ->filter($this->getUserSettingsOptionSQL('FILTER', $this->getMyUserPreferencesDefault('FILTER')))
1489
            ->map('ID', 'ID')->toArray();
1490
        $alsoShowProductsArray[0] = 0;
1491
        $parentIDs = Product::get()->filter(array('ID' => $alsoShowProductsArray))->map('ParentID', 'ParentID')->toArray();
1492
        //just in case
1493
        unset($parentIDs[$this->ID]);
1494
        if (! count($parentIDs)) {
1495
            $parentIDs = array(0 => 0);
1496
        }
1497
1498
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInMenus' => 1));
1499
    }
1500
1501
    /**
1502
     * given the products for this page,
1503
     * retrieve the parent groups excluding the current one.
1504
     *
1505
     * @return DataList
1506
     */
1507
    public function ProductGroupsParentGroups()
1508
    {
1509
        $arrayOfIDs = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER')) + array(0 => 0);
1510
        $parentIDs = Product::get()->filter(array('ID' => $arrayOfIDs))->map('ParentID', 'ParentID')->toArray();
1511
        //just in case
1512
        unset($parentIDs[$this->ID]);
1513
        if (! count($parentIDs)) {
1514
            $parentIDs = array(0 => 0);
1515
        }
1516
1517
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1518
    }
1519
1520
    /**
1521
     * returns stage as "" or "_Live".
1522
     *
1523
     * @return string
1524
     */
1525
    protected function getStage()
1526
    {
1527
        $stage = '';
1528
        if (Versioned::current_stage() == 'Live') {
1529
            $stage = '_Live';
1530
        }
1531
1532
        return $stage;
1533
    }
1534
1535
    /*****************************************************
1536
     * STANDARD SS METHODS
1537
     *****************************************************/
1538
1539
    /**
1540
     * tells us if the current page is part of e-commerce.
1541
     *
1542
     * @return bool
1543
     */
1544
    public function IsEcommercePage()
1545
    {
1546
        return true;
1547
    }
1548
1549
    public function onAfterWrite()
1550
    {
1551
        parent::onAfterWrite();
1552
1553
        if ($this->ImageID) {
1554
            if ($normalImage = Image::get()->exclude(array('ClassName' => 'Product_Image'))->byID($this->ImageID)) {
1555
                $normalImage = $normalImage->newClassInstance('Product_Image');
1556
                $normalImage->write();
1557
            }
1558
        }
1559
    }
1560
1561
    public function requireDefaultRecords()
1562
    {
1563
        parent::requireDefaultRecords();
1564
        $urlSegments = ProductGroup::get()->column('URLSegment');
1565
        foreach ($urlSegments as $urlSegment) {
1566
            $counts = array_count_values($urlSegments);
1567
            $hasDuplicates = $counts[$urlSegment]  > 1 ? true : false;
1568
            if ($hasDuplicates) {
1569
                DB::alteration_message('found duplicates for '.$urlSegment, 'deleted');
1570
                $checkForDuplicatesURLSegments = ProductGroup::get()
1571
                    ->filter(array('URLSegment' => $urlSegment));
1572
                if ($checkForDuplicatesURLSegments->count()) {
1573
                    $count = 0;
1574
                    foreach ($checkForDuplicatesURLSegments as $productGroup) {
1575
                        if ($count > 0) {
1576
                            $oldURLSegment = $productGroup->URLSegment;
1577
                            DB::alteration_message(' ... Correcting URLSegment for '.$productGroup->Title.' with ID: '.$productGroup->ID, 'deleted');
1578
                            $productGroup->writeToStage('Stage');
1579
                            $productGroup->publish('Stage', 'Live');
1580
                            $newURLSegment = $productGroup->URLSegment;
1581
                            DB::alteration_message(' ... .... from '.$oldURLSegment.' to '.$newURLSegment, 'created');
1582
                        }
1583
                        $count++;
1584
                    }
1585
                }
1586
            }
1587
        }
1588
    }
1589
1590
    /*****************************************************
1591
     * CACHING
1592
     *****************************************************/
1593
    /**
1594
     *
1595
     * @return bool
1596
     */
1597
    public function AllowCaching()
1598
    {
1599
        return $this->allowCaching;
1600
    }
1601
1602
    /**
1603
     * keeps a cache of the common caching key element
1604
     * @var string
1605
     */
1606
    private static $_product_group_cache_key_cache = null;
1607
1608
    /**
1609
     *
1610
     * @param string $name
0 ignored issues
show
Bug introduced by
There is no parameter named $name. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1611
     * @param string $filterKey
0 ignored issues
show
Bug introduced by
There is no parameter named $filterKey. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1612
     *
1613
     * @return string
1614
     */
1615
    public function cacheKey($cacheKey)
1616
    {
1617
        $cacheKey = $cacheKey.'_'.$this->ID;
1618
        if (self::$_product_group_cache_key_cache === null) {
1619
            self::$_product_group_cache_key_cache = "_PR_"
1620
                .strtotime(Product::get()->max('LastEdited')). "_"
1621
                .Product::get()->count();
1622
            self::$_product_group_cache_key_cache .= "PG_"
1623
                .strtotime(ProductGroup::get()->max('LastEdited')). "_"
1624
                .ProductGroup::get()->count();
1625
            if (class_exists('ProductVariation')) {
1626
                self::$_product_group_cache_key_cache .= "PV_"
1627
                  .strtotime(ProductVariation::get()->max('LastEdited')). "_"
1628
                  .ProductVariation::get()->count();
1629
            }
1630
        }
1631
        $cacheKey .= self::$_product_group_cache_key_cache;
1632
1633
        return $cacheKey;
1634
    }
1635
1636
    /**
1637
     * @var Zend_Cache_Core
1638
     */
1639
    protected $silverstripeCoreCache = null;
1640
1641
    /**
1642
     * Set the cache object to use when storing / retrieving partial cache blocks.
1643
     *
1644
     * @param Zend_Cache_Core $silverstripeCoreCache
1645
     */
1646
    public function setSilverstripeCoreCache($silverstripeCoreCache)
1647
    {
1648
        $this->silverstripeCoreCache = $silverstripeCoreCache;
1649
    }
1650
1651
    /**
1652
     * Get the cache object to use when storing / retrieving stuff in the Silverstripe Cache
1653
     *
1654
     * @return Zend_Cache_Core
1655
     */
1656
    protected function getSilverstripeCoreCache()
1657
    {
1658
        return $this->silverstripeCoreCache ? $this->silverstripeCoreCache : SS_Cache::factory('EcomPG');
1659
    }
1660
1661
    /**
1662
     * saving an object to the.
1663
     *
1664
     * @param string $cacheKey
1665
     *
1666
     * @return mixed
1667
     */
1668
    protected function retrieveObjectStore($cacheKey)
1669
    {
1670
        $cacheKey = $this->cacheKey($cacheKey);
1671
        if ($this->AllowCaching()) {
1672
            $cache = $this->getSilverstripeCoreCache();
1673
            $data = $cache->load($cacheKey);
1674
            if (!$data) {
1675
                return;
1676
            }
1677
            if (! $cache->getOption('automatic_serialization')) {
1678
                $data = @unserialize($data);
1679
            }
1680
            return $data;
1681
        }
1682
1683
        return;
1684
    }
1685
1686
    /**
1687
     * returns true when the data is saved...
1688
     *
1689
     * @param mixed  $data
1690
     * @param string $cacheKey - key under which the data is saved...
1691
     *
1692
     * @return bool
1693
     */
1694
    protected function saveObjectStore($data, $cacheKey)
1695
    {
1696
        $cacheKey = $this->cacheKey($cacheKey);
1697
        if ($this->AllowCaching()) {
1698
            $cache = $this->getSilverstripeCoreCache();
1699
            if (! $cache->getOption('automatic_serialization')) {
1700
                $data = serialize($data);
1701
            }
1702
            $cache->save($data, $cacheKey);
1703
            return true;
1704
        }
1705
1706
        return false;
1707
    }
1708
1709
    /**
1710
     *
1711
     * @param Boolean $isForGroups OPTIONAL
1712
     *
1713
     * @return string
1714
     */
1715
    public function SearchResultsSessionVariable($isForGroups = false)
1716
    {
1717
        $idString = '_'.$this->ID;
1718
        if ($isForGroups) {
1719
            return Config::inst()->get('ProductSearchForm', 'product_session_variable').$idString;
1720
        } else {
1721
            return Config::inst()->get('ProductSearchForm', 'product_group_session_variable').$idString;
1722
        }
1723
    }
1724
1725
    /**
1726
     * cache for result array.
1727
     *
1728
     * @var array
1729
     */
1730
    private static $_result_array = array();
1731
1732
    /**
1733
     * @return array
1734
     */
1735
    public function searchResultsArrayFromSession()
1736
    {
1737
        if (! isset(self::$_result_array[$this->ID]) || self::$_result_array[$this->ID] === null) {
1738
            self::$_result_array[$this->ID] = explode(',', Session::get($this->SearchResultsSessionVariable(false)));
1739
        }
1740
        if (! is_array(self::$_result_array[$this->ID]) || ! count(self::$_result_array[$this->ID])) {
1741
            self::$_result_array[$this->ID] = array(0 => 0);
1742
        }
1743
1744
        return self::$_result_array[$this->ID];
1745
    }
1746
1747
    public function getNumberOfProducts()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1748
    {
1749
        return Product::get()->filter(array('ParentID' => $this->ID))->count();
1750
    }
1751
}
1752
1753
class ProductGroup_Controller extends Page_Controller
1754
{
1755
    /**
1756
     * standard SS variable.
1757
     *
1758
     * @var array
1759
     */
1760
    private static $allowed_actions = array(
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions 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...
1761
        'debug' => 'ADMIN',
1762
        'filterforgroup' => true,
1763
        'ProductSearchForm' => true,
1764
        'searchresults' => true,
1765
        'resetfilter' => true,
1766
    );
1767
1768
    /**
1769
     * The original Title of this page before filters, etc...
1770
     *
1771
     * @var string
1772
     */
1773
    protected $originalTitle = '';
1774
1775
    /**
1776
     * list of products that are going to be shown.
1777
     *
1778
     * @var DataList
1779
     */
1780
    protected $products = null;
1781
1782
    /**
1783
     * Show all products on one page?
1784
     *
1785
     * @var bool
1786
     */
1787
    protected $showFullList = false;
1788
1789
    /**
1790
     * The group filter that is applied to this page.
1791
     *
1792
     * @var ProductGroup
1793
     */
1794
    protected $filterForGroupObject = null;
1795
1796
    /**
1797
     * Is this a product search?
1798
     *
1799
     * @var bool
1800
     */
1801
    protected $isSearchResults = false;
1802
1803
    /**
1804
     * standard SS method.
1805
     */
1806
    public function init()
1807
    {
1808
        parent::init();
1809
        $this->originalTitle = $this->Title;
1810
        Requirements::themedCSS('ProductGroup', 'ecommerce');
1811
        Requirements::themedCSS('ProductGroupPopUp', 'ecommerce');
1812
        Requirements::javascript('ecommerce/javascript/EcomProducts.js');
1813
        //we save data from get variables...
1814
        $this->saveUserPreferences();
1815
        //makes sure best match only applies to search -i.e. reset otherwise.
1816
        if($this->request->param('Action') !== 'searchresults') {
1817
            $sortKey = $this->getCurrentUserPreferences('SORT');
1818
            if ($sortKey === Config::inst()->get('ProductGroupSearchPage', 'best_match_key')) {
1819
                $this->resetsort();
1820
            }
1821
        }
1822
    }
1823
1824
    /****************************************************
1825
     *  ACTIONS
1826
    /****************************************************/
1827
1828
    /**
1829
     * standard selection of products.
1830
     */
1831
    public function index()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1832
    {
1833
        //set the filter and the sort...
1834
        $this->addSecondaryTitle();
1835
        $this->products = $this->paginateList($this->ProductsShowable(null));
1836
        if ($this->returnAjaxifiedProductList()) {
1837
            return $this->renderWith('AjaxProductList');
1838
        }
1839
        return array();
1840
    }
1841
1842
    /**
1843
     * cross filter with another product group..
1844
     *
1845
     * e.g. socks (current product group) for brand A or B (the secondary product group)
1846
     *
1847
     * @param HTTPRequest
1848
     */
1849
    public function filterforgroup($request)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1850
    {
1851
        $this->resetfilter();
1852
        $otherGroupURLSegment = Convert::raw2sql($request->param('ID'));
1853
        $arrayOfIDs = array(0 => 0);
1854
        if ($otherGroupURLSegment) {
1855
            $otherProductGroup = DataObject::get_one(
1856
                'ProductGroup',
1857
                array('URLSegment' => $otherGroupURLSegment)
1858
            );
1859
            if ($otherProductGroup) {
1860
                $this->filterForGroupObject = $otherProductGroup;
1861
                $arrayOfIDs = $otherProductGroup->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1862
            }
1863
        }
1864
        $this->addSecondaryTitle();
1865
        $this->products = $this->paginateList($this->ProductsShowable(array('ID' => $arrayOfIDs)));
1866
        if ($this->returnAjaxifiedProductList()) {
1867
            return $this->renderWith('AjaxProductList');
1868
        }
1869
1870
        return array();
1871
    }
1872
1873
1874
    /**
1875
     * get the search results.
1876
     *
1877
     * @param HTTPRequest
1878
     */
1879
    public function searchresults($request)
1880
    {
1881
        $this->isSearchResults = true;
1882
        //filters are irrelevant right now
1883
        $this->resetfilter();
1884
        //get results array
1885
        $resultArray = $this->searchResultsArrayFromSession();
1886
        $hasResults =  (is_array($resultArray)  && count($resultArray)) ? true : false;
1887
        $suggestion = Config::inst()->get('ProductGroupSearchPage', 'best_match_key');
1888
        $alternativeSort = $this->getAlternativeSort($suggestion);
1889
        if (! $hasResults) {
1890
            $resultArray = array(0 => 0);
1891
        }
1892
        $title = ProductSearchForm::get_last_search_phrase();
1893
        if ($title) {
1894
            $title = _t('Ecommerce.SEARCH_FOR', 'search for: ').substr($title, 0, 25);
1895
        }
1896
        $this->addSecondaryTitle($title);
1897
        $this->products = $this->paginateList(
1898
            $this->ProductsShowable(
1899
                ['ID' => $resultArray],
1900
                $alternativeSort
1901
            )
1902
        );
1903
1904
        return array();
1905
    }
1906
1907
    protected function getAlternativeSort($suggestion = '')
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1908
    {
1909
        $sortGetVariable = $this->getSortFilterDisplayNames('SORT', 'getVariable');
1910
        if(! $this->request->getVar($sortGetVariable)) {
1911
            if($suggestion) {
1912
                $this->saveUserPreferences(
1913
                    [
1914
                        $sortGetVariable => $suggestion
1915
                    ]
1916
                );
1917
1918
                return $this->createSortStatementFromIDArray($resultArray);
0 ignored issues
show
Bug introduced by
The variable $resultArray does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1919
            }
1920
        }
1921
    }
1922
1923
    /**
1924
     * resets the filter only.
1925
     */
1926
    public function resetfilter()
1927
    {
1928
        $defaultKey = $this->getMyUserPreferencesDefault('FILTER');
1929
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
1930
        $this->saveUserPreferences(
1931
            array(
1932
                $filterGetVariable => $defaultKey,
1933
            )
1934
        );
1935
1936
        return array();
1937
    }
1938
    /**
1939
     * resets the filter only.
1940
     */
1941
    public function resetsort()
1942
    {
1943
        $defaultKey = $this->getMyUserPreferencesDefault('SORT');
1944
        $sortGetVariable = $this->getSortFilterDisplayNames('SORT', 'getVariable');
1945
        $this->saveUserPreferences(
1946
            array(
1947
                $sortGetVariable => $defaultKey,
1948
            )
1949
        );
1950
1951
        return array();
1952
    }
1953
1954
    /****************************************************
1955
     *  TEMPLATE METHODS PRODUCTS
1956
    /****************************************************/
1957
1958
    /**
1959
     * Return the products for this group.
1960
     * This is the call that is made from the template...
1961
     * The actual final products being shown.
1962
     *
1963
     * @return DataList
1964
     **/
1965
    public function Products()
1966
    {
1967
        //IMPORTANT!
1968
        //two universal actions!
1969
        $this->addSecondaryTitle();
1970
        $this->cachingRelatedJavascript();
1971
1972
        //save products to session for later use
1973
        $stringOfIDs = '';
1974
        $array = $this->getProductsThatCanBePurchasedArray();
1975
        if (is_array($array)) {
1976
            $stringOfIDs = implode(',', $array);
1977
        }
1978
        //save list for future use
1979
        Session::set(EcommerceConfig::get('ProductGroup', 'session_name_for_product_array'), $stringOfIDs);
1980
1981
        return $this->products;
1982
    }
1983
1984
    /**
1985
     * you can overload this function of ProductGroup Extensions.
1986
     *
1987
     * @return bool
1988
     */
1989
    protected function returnAjaxifiedProductList()
1990
    {
1991
        return Director::is_ajax() ? true : false;
1992
    }
1993
1994
    /**
1995
     * is the product list cache-able?
1996
     *
1997
     * @return bool
1998
     */
1999
    public function ProductGroupListAreCacheable()
2000
    {
2001
        if ($this->productListsHTMLCanBeCached()) {
2002
            //exception 1
2003
            if ($this->IsSearchResults()) {
2004
                return false;
2005
            }
2006
            //exception 2
2007
            $currentOrder = ShoppingCart::current_order();
2008
            if ($currentOrder->getHasAlternativeCurrency()) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !$currentOrder->g...sAlternativeCurrency();.
Loading history...
2009
                return false;
2010
            }
2011
            //can be cached...
2012
            return true;
2013
        }
2014
2015
        return false;
2016
    }
2017
2018
    /**
2019
     * is the product list ajaxified.
2020
     *
2021
     * @return bool
2022
     */
2023
    public function ProductGroupListAreAjaxified()
2024
    {
2025
        return $this->IsSearchResults() ? false : true;
2026
    }
2027
2028
    /**
2029
     * Unique caching key for the product list...
2030
     *
2031
     * @return string | Null
2032
     */
2033
    public function ProductGroupListCachingKey()
2034
    {
2035
        if ($this->ProductGroupListAreCacheable()) {
2036
            $displayKey = $this->getCurrentUserPreferences('DISPLAY');
2037
            $filterKey = $this->getCurrentUserPreferences('FILTER');
2038
            $filterForGroupKey = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
2039
            $sortKey = $this->getCurrentUserPreferences('SORT');
2040
            $pageStart = $this->request->getVar('start') ? intval($this->request->getVar('start')) : 0;
2041
            $isFullList = $this->IsShowFullList() ? 'Y' : 'N';
2042
            return $this->cacheKey(
2043
                implode(
2044
                    '_',
2045
                    array(
2046
                        $displayKey,
2047
                        $filterKey,
2048
                        $filterForGroupKey,
2049
                        $sortKey,
2050
                        $pageStart,
2051
                        $isFullList,
2052
                    )
2053
                )
2054
            );
2055
        }
2056
2057
        return;
2058
    }
2059
2060
    /**
2061
     * adds Javascript to the page to make it work when products are cached.
2062
     */
2063
    public function CachingRelatedJavascript()
2064
    {
2065
        if ($this->ProductGroupListAreAjaxified()) {
2066
            Requirements::customScript(
2067
                "
2068
                    if(typeof EcomCartOptions === 'undefined') {
2069
                        var EcomCartOptions = {};
2070
                    }
2071
                    EcomCartOptions.ajaxifyProductList = true;
2072
                    EcomCartOptions.ajaxifiedListHolderSelector = '#".$this->AjaxDefinitions()->ProductListHolderID()."';
2073
                    EcomCartOptions.ajaxifiedListAdjusterSelectors = '.".$this->AjaxDefinitions()->ProductListAjaxifiedLinkClassName()."';
2074
                    EcomCartOptions.hiddenPageTitleID = '#".$this->AjaxDefinitions()->HiddenPageTitleID()."';
2075
                ",
2076
                'cachingRelatedJavascript_AJAXlist'
2077
            );
2078
        } else {
2079
            Requirements::customScript(
2080
                "
2081
                    if(typeof EcomCartOptions === 'undefined') {
2082
                        var EcomCartOptions = {};
2083
                    }
2084
                    EcomCartOptions.ajaxifyProductList = false;
2085
                ",
2086
                'cachingRelatedJavascript_AJAXlist'
2087
            );
2088
        }
2089
        $currentOrder = ShoppingCart::current_order();
2090
        if ($currentOrder->TotalItems(true)) {
2091
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
2092
            $obj = new $responseClass();
2093
            $obj->setIncludeHeaders(false);
2094
            $json = $obj->ReturnCartData();
2095
            Requirements::customScript(
2096
                "
2097
                    if(typeof EcomCartOptions === 'undefined') {
2098
                        var EcomCartOptions = {};
2099
                    }
2100
                    EcomCartOptions.initialData= ".$json.";
2101
                ",
2102
                'cachingRelatedJavascript_JSON'
2103
            );
2104
        }
2105
    }
2106
2107
    /**
2108
     * you can overload this function of ProductGroup Extensions.
2109
     *
2110
     * @return bool
2111
     */
2112
    protected function productListsHTMLCanBeCached()
2113
    {
2114
        return Config::inst()->get('ProductGroup', 'actively_check_for_can_purchase') ? false : true;
2115
    }
2116
2117
    /*****************************************************
2118
     * DATALIST: totals, number per page, etc..
2119
     *****************************************************/
2120
2121
    /**
2122
     * returns the total numer of products (before pagination).
2123
     *
2124
     * @return bool
2125
     **/
2126
    public function TotalCountGreaterThanOne($greaterThan = 1)
2127
    {
2128
        return $this->TotalCount() > $greaterThan;
2129
    }
2130
2131
    /**
2132
     * have the ProductsShowable been limited.
2133
     *
2134
     * @return bool
2135
     **/
2136
    public function TotalCountGreaterThanMax()
2137
    {
2138
        return $this->RawCount() >  $this->TotalCount();
2139
    }
2140
2141
    /****************************************************
2142
     *  TEMPLATE METHODS MENUS AND SIDEBARS
2143
    /****************************************************/
2144
2145
    /**
2146
     * title without additions.
2147
     *
2148
     * @return string
2149
     */
2150
    public function OriginalTitle()
2151
    {
2152
        return $this->originalTitle;
2153
    }
2154
    /**
2155
     * This method can be extended to show products in the side bar.
2156
     */
2157
    public function SidebarProducts()
2158
    {
2159
        return;
2160
    }
2161
2162
    /**
2163
     * returns child product groups for use in
2164
     * 'in this section'. For example the vegetable Product Group
2165
     * May have listed here: Carrot, Cabbage, etc...
2166
     *
2167
     * @return ArrayList (ProductGroups)
2168
     */
2169
    public function MenuChildGroups()
2170
    {
2171
        return $this->ChildGroups(2, '"ShowInMenus" = 1');
2172
    }
2173
2174
    /**
2175
     * After a search is conducted you may end up with a bunch
2176
     * of recommended product groups. They will be returned here...
2177
     * We sort the list in the order that it is provided.
2178
     *
2179
     * @return DataList | Null (ProductGroups)
2180
     */
2181
    public function SearchResultsChildGroups()
2182
    {
2183
        $groupArray = explode(',', Session::get($this->SearchResultsSessionVariable($isForGroup = true)));
2184
        if (is_array($groupArray) && count($groupArray)) {
2185
            $sortStatement = $this->createSortStatementFromIDArray($groupArray, 'ProductGroup');
2186
2187
            return ProductGroup::get()->filter(array('ID' => $groupArray, 'ShowInSearch' => 1))->sort($sortStatement);
2188
        }
2189
2190
        return;
2191
    }
2192
2193
    /****************************************************
2194
     *  Search Form Related controllers
2195
    /****************************************************/
2196
2197
    /**
2198
     * returns a search form to search current products.
2199
     *
2200
     * @return ProductSearchForm object
2201
     */
2202
    public function ProductSearchForm()
2203
    {
2204
        $onlySearchTitle = $this->originalTitle;
2205
        if ($this->dataRecord instanceof ProductGroupSearchPage) {
2206
            if ($this->HasSearchResults()) {
2207
                $onlySearchTitle = 'Last Search Results';
2208
            }
2209
        }
2210
        $form = ProductSearchForm::create(
2211
            $this,
2212
            'ProductSearchForm',
2213
            $onlySearchTitle,
2214
            $this->currentInitialProducts(null, $this->getMyUserPreferencesDefault('FILTER'))
2215
        );
2216
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2217
        $sortGetVariable = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2218
        $additionalGetParameters =
2219
            $filterGetVariable.'='.$this->getMyUserPreferencesDefault('FILTER').'&'.
2220
            $sortGetVariable.'='.Config::inst()->get('ProductGroupSearchPage', 'best_match_key');
2221
        $form->setAdditionalGetParameters($additionalGetParameters);
2222
2223
        return $form;
2224
    }
2225
2226
    /**
2227
     * Does this page have any search results?
2228
     * If search was carried out without returns
2229
     * then it returns zero (false).
2230
     *
2231
     * @return int | false
2232
     */
2233
    public function HasSearchResults()
2234
    {
2235
        $resultArray = $this->searchResultsArrayFromSession();
2236
        if ($resultArray) {
2237
            $count = count($resultArray) - 1;
2238
2239
            return $count ? $count : 0;
2240
        }
2241
2242
        return 0;
2243
    }
2244
2245
    /**
2246
     * Should the product search form be shown immediately?
2247
     *
2248
     * @return bool
2249
     */
2250
    public function ShowSearchFormImmediately()
2251
    {
2252
        if ($this->IsSearchResults()) {
2253
            return true;
2254
        }
2255
        if ((!$this->products) || ($this->products && $this->products->count())) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !(!$this->product...is->products->count());.
Loading history...
2256
            return false;
2257
        }
2258
2259
        return true;
2260
    }
2261
2262
    /**
2263
     * Show a search form on this page?
2264
     *
2265
     * @return bool
2266
     */
2267
    public function ShowSearchFormAtAll()
2268
    {
2269
        return true;
2270
    }
2271
2272
    /**
2273
     * Is the current page a display of search results.
2274
     *
2275
     * This does not mean that something is actively being search for,
2276
     * it could also be just "showing the search results"
2277
     *
2278
     * @return bool
2279
     */
2280
    public function IsSearchResults()
2281
    {
2282
        return $this->isSearchResults;
2283
    }
2284
2285
    /**
2286
     * Is there something actively being searched for?
2287
     *
2288
     * This is different from IsSearchResults.
2289
     *
2290
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2291
     */
2292
    public function ActiveSearchTerm()
2293
    {
2294
        $data = Session::get(Config::inst()->get('ProductSearchForm', 'form_data_session_variable'));
2295
        if (!empty($data['Keyword'])) {
2296
            return $this->IsSearchResults();
2297
        }
2298
    }
2299
2300
    /****************************************************
2301
     *  Filter / Sort / Display related controllers
2302
    /****************************************************/
2303
2304
    /**
2305
     * Do we show all products on one page?
2306
     *
2307
     * @return bool
2308
     */
2309
    public function ShowFiltersAndDisplayLinks()
2310
    {
2311
        if ($this->TotalCountGreaterThanOne()) {
2312
            if ($this->HasFilters()) {
2313
                return true;
2314
            }
2315
            if ($this->DisplayLinks()) {
2316
                return true;
2317
            }
2318
        }
2319
2320
        return false;
2321
    }
2322
2323
    /**
2324
     * Do we show the sort links.
2325
     *
2326
     * A bit arbitrary to say three,
2327
     * but there is not much point to sort three or less products
2328
     *
2329
     * @return bool
2330
     */
2331
    public function ShowSortLinks($minimumCount = 3)
2332
    {
2333
        if ($this->TotalCountGreaterThanOne($minimumCount)) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $this->TotalCount...ThanOne($minimumCount);.
Loading history...
2334
            return true;
2335
        }
2336
2337
        return false;
2338
    }
2339
2340
    /**
2341
     * Is there a special filter operating at the moment?
2342
     * Is the current filter the default one (return inverse!)?
2343
     *
2344
     * @return bool
2345
     */
2346
    public function HasFilter()
2347
    {
2348
        return $this->getCurrentUserPreferences('FILTER') != $this->getMyUserPreferencesDefault('FILTER')
2349
        || $this->filterForGroupObject;
2350
    }
2351
2352
    /**
2353
     * Is there a special sort operating at the moment?
2354
     * Is the current sort the default one (return inverse!)?
2355
     *
2356
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2357
     */
2358
    public function HasSort()
2359
    {
2360
        $sort = $this->getCurrentUserPreferences('SORT');
2361
        if ($sort != $this->getMyUserPreferencesDefault('SORT')) {
2362
            return true;
2363
        }
2364
    }
2365
2366
    /**
2367
     * @return boolean
2368
     */
2369
    public function HasFilterOrSort()
2370
    {
2371
        return $this->HasFilter() || $this->HasSort();
2372
    }
2373
2374
    /**
2375
     * @return boolean
2376
     */
2377
    public function HasFilterOrSortFullList()
2378
    {
2379
        return $this->HasFilterOrSort() || $this->IsShowFullList();
2380
    }
2381
2382
    /**
2383
     * are filters available?
2384
     * we check one at the time so that we do the least
2385
     * amount of DB queries.
2386
     *
2387
     * @return bool
2388
     */
2389
    public function HasFilters()
2390
    {
2391
        $countFilters = $this->FilterLinks()->count();
2392
        if ($countFilters > 1) {
2393
            return true;
2394
        }
2395
        $countGroupFilters = $this->ProductGroupFilterLinks()->count();
2396
        if ($countGroupFilters > 1) {
2397
            return true;
2398
        }
2399
        if ($countFilters + $countGroupFilters > 1) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return $countFilters + $countGroupFilters > 1;.
Loading history...
2400
            return true;
2401
        }
2402
2403
        return false;
2404
    }
2405
2406
    /**
2407
     * Do we show all products on one page?
2408
     *
2409
     * @return bool
2410
     */
2411
    public function IsShowFullList()
2412
    {
2413
        return $this->showFullList;
2414
    }
2415
2416
    /**
2417
     * returns the current filter applied to the list
2418
     * in a human readable string.
2419
     *
2420
     * @return string
2421
     */
2422
    public function CurrentDisplayTitle()
2423
    {
2424
        $displayKey = $this->getCurrentUserPreferences('DISPLAY');
2425
        if ($displayKey != $this->getMyUserPreferencesDefault('DISPLAY')) {
2426
            return $this->getUserPreferencesTitle('DISPLAY', $displayKey);
2427
        }
2428
    }
2429
2430
    /**
2431
     * returns the current filter applied to the list
2432
     * in a human readable string.
2433
     *
2434
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2435
     */
2436
    public function CurrentFilterTitle()
2437
    {
2438
        $filterKey = $this->getCurrentUserPreferences('FILTER');
2439
        $filters = array();
2440
        if ($filterKey != $this->getMyUserPreferencesDefault('FILTER')) {
2441
            $filters[] = $this->getUserPreferencesTitle('FILTER', $filterKey);
2442
        }
2443
        if ($this->filterForGroupObject) {
2444
            $filters[] = $this->filterForGroupObject->MenuTitle;
2445
        }
2446
        if (count($filters)) {
2447
            return implode(', ', $filters);
2448
        }
2449
    }
2450
2451
    /**
2452
     * returns the current sort applied to the list
2453
     * in a human readable string.
2454
     *
2455
     * @return string
2456
     */
2457
    public function CurrentSortTitle()
2458
    {
2459
        $sortKey = $this->getCurrentUserPreferences('SORT');
2460
        if ($sortKey != $this->getMyUserPreferencesDefault('SORT')) {
2461
            return $this->getUserPreferencesTitle('SORT', $sortKey);
2462
        }
2463
    }
2464
2465
    /**
2466
     * short-cut for getMyUserPreferencesDefault("DISPLAY")
2467
     * for use in templtes.
2468
     *
2469
     * @return string - key
2470
     */
2471
    public function MyDefaultDisplayStyle()
2472
    {
2473
        return $this->getMyUserPreferencesDefault('DISPLAY');
2474
    }
2475
2476
    /**
2477
     * Number of entries per page limited by total number of pages available...
2478
     *
2479
     * @return int
2480
     */
2481
    public function MaxNumberOfProductsPerPage()
2482
    {
2483
        return $this->MyNumberOfProductsPerPage() > $this->TotalCount() ? $this->TotalCount() : $this->MyNumberOfProductsPerPage();
2484
    }
2485
2486
    /****************************************************
2487
     *  TEMPLATE METHODS FILTER LINK
2488
    /****************************************************/
2489
2490
    /**
2491
     * Provides a ArrayList of links for filters products.
2492
     *
2493
     * @return ArrayList( ArrayData(Name, Link, SelectKey, Current (boolean), LinkingMode))
0 ignored issues
show
Documentation introduced by
The doc-type ArrayList( could not be parsed: Expected "|" or "end of type", but got "(" at position 9. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2494
     */
2495
    public function FilterLinks()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
2496
    {
2497
        $cacheKey = 'FilterLinks_'.($this->filterForGroupObject ? $this->filterForGroupObject->ID : 0);
2498
        if ($list = $this->retrieveObjectStore($cacheKey)) {
2499
            //do nothing
2500
        } else {
2501
            $list = $this->userPreferencesLinks('FILTER');
2502
            foreach ($list as $obj) {
0 ignored issues
show
Bug introduced by
The expression $list of type null|this<ProductGroup_Controller> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2503
                $key = $obj->SelectKey;
2504
                if ($key != $this->getMyUserPreferencesDefault('FILTER')) {
2505
                    $count = count($this->currentInitialProductsAsCachedArray($key));
2506
                    if ($count == 0) {
2507
                        $list->remove($obj);
2508
                    } else {
2509
                        $obj->Count = $count;
2510
                    }
2511
                }
2512
            }
2513
            $this->saveObjectStore($list, $cacheKey);
2514
        }
2515
        $selectedItem = $this->getCurrentUserPreferences('FILTER');
2516
        foreach ($list as $obj) {
2517
            $canHaveCurrent = true;
2518
            if ($this->filterForGroupObject) {
2519
                $canHaveCurrent = false;
2520
            }
2521
            $obj->Current = $selectedItem == $obj->SelectKey && $canHaveCurrent ? true : false;
2522
            $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2523
            $obj->Ajaxify = true;
2524
        }
2525
2526
        return $list;
2527
    }
2528
2529
    /**
2530
     * returns a list of items (with links).
2531
     *
2532
     * @return ArrayList( ArrayData(Name, FilterLink,  SelectKey, Current (boolean), LinkingMode))
0 ignored issues
show
Documentation introduced by
The doc-type ArrayList( could not be parsed: Expected "|" or "end of type", but got "(" at position 9. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2533
     */
2534
    public function ProductGroupFilterLinks()
2535
    {
2536
        if ($array = $this->retrieveObjectStore('ProductGroupFilterLinks')) {
2537
            //do nothing
2538
        } else {
2539
            $arrayOfItems = array();
2540
2541
            $baseArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2542
2543
            //also show
2544
            $items = $this->ProductGroupsFromAlsoShowProducts();
2545
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2546
            //also show inverse
2547
            $items = $this->ProductGroupsFromAlsoShowProductsInverse();
2548
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2549
2550
            //parent groups
2551
            $items = $this->ProductGroupsParentGroups();
2552
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2553
2554
            //child groups
2555
            $items = $this->MenuChildGroups();
2556
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2557
2558
            ksort($arrayOfItems);
2559
            $array = array();
2560
            foreach ($arrayOfItems as $arrayOfItem) {
2561
                $array[] = $this->makeArrayItem($arrayOfItem);
2562
            }
2563
            $this->saveObjectStore($array, 'ProductGroupFilterLinks');
2564
        }
2565
        $arrayList = ArrayList::create();
2566
        foreach ($array as $item) {
2567
            $arrayList->push(ArrayData::create($item));
2568
        }
2569
2570
        return $arrayList;
2571
    }
2572
2573
2574
    /**
2575
     * @see ProductGroupFilterLinks
2576
     * same as ProductGroupFilterLinks, but with originating Object...
2577
     *
2578
     * @return ArrayList
2579
     */
2580
    public function ProductGroupFilterOriginalObjects()
2581
    {
2582
        $links = $this->ProductGroupFilterLinks();
2583
        // /print_r($links);
2584
        foreach ($links as $linkItem) {
2585
            $className = $linkItem->ClassName;
2586
            $id = $linkItem->ID;
2587
            if ($className && $id) {
2588
                $object = $className::get()->byID($id);
2589
                $linkItem->Object = $object;
2590
            }
2591
        }
2592
2593
2594
        return $links;
2595
    }
2596
2597
    /**
2598
     * counts the total number in the combination....
2599
     *
2600
     * @param DataList $items     - list of
2601
     * @param Arary    $baseArray - list of products on the current page
2602
     *
2603
     * @return array
2604
     */
2605
    protected function productGroupFilterLinksCount($items, $baseArray, $ajaxify = true)
2606
    {
2607
        $array = array();
2608
        if ($items && $items->count()) {
2609
            foreach ($items as $item) {
2610
                $arrayOfIDs = $item->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2611
                $newArray = array_intersect_key(
2612
                    $arrayOfIDs,
2613
                    $baseArray
2614
                );
2615
                $count = count($newArray);
2616
                if ($count) {
2617
                    $array[$item->Title] = array(
2618
                        'Item' => $item,
2619
                        'Count' => $count,
2620
                        'Ajaxify' => $ajaxify,
2621
                    );
2622
                }
2623
            }
2624
        }
2625
2626
        return $array;
2627
    }
2628
2629
    /**
2630
     * @param array itemInArray (Item, Count, UserFilterAction)
2631
     *
2632
     * @return ArrayData
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2633
     */
2634
    protected function makeArrayItem($itemInArray)
2635
    {
2636
        $item = $itemInArray['Item'];
2637
        $count = $itemInArray['Count'];
2638
        $ajaxify = $itemInArray['Ajaxify'];
2639
        $filterForGroupObjectID = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
2640
        $isCurrent = ($item->ID == $filterForGroupObjectID ? true : false);
2641
        if ($ajaxify) {
2642
            $link = $this->Link($item->FilterForGroupLinkSegment());
2643
        } else {
2644
            $link = $item->Link();
2645
        }
2646
        return array(
2647
            'ID' => $item->ID,
2648
            'ClassName' => $item->ClassName,
2649
            'Title' => $item->Title,
2650
            'Count' => $count,
2651
            'SelectKey' => $item->URLSegment,
2652
            'Current' => $isCurrent ? true : false,
2653
            'MyLinkingMode' => $isCurrent ? 'current' : 'link',
2654
            'FilterLink' => $link,
2655
            'Ajaxify' => $ajaxify ? true : false,
2656
        );
2657
    }
2658
2659
    /**
2660
     * Provides a ArrayList of links for sorting products.
2661
     */
2662
    public function SortLinks()
2663
    {
2664
2665
        $list = $this->userPreferencesLinks('SORT');
2666
        $selectedItem = $this->getCurrentUserPreferences('SORT');
2667
        if ($list) {
2668
            foreach ($list as $obj) {
2669
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2670
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2671
                $obj->Ajaxify = true;
2672
            }
2673
2674
            return $list;
2675
        }
2676
    }
2677
2678
    /**
2679
     * Provides a ArrayList for displaying display links.
2680
     */
2681
    public function DisplayLinks()
2682
    {
2683
        $list = $this->userPreferencesLinks('DISPLAY');
2684
        $selectedItem = $this->getCurrentUserPreferences('DISPLAY');
2685
        if ($list) {
2686
            foreach ($list as $obj) {
2687
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2688
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2689
                $obj->Ajaxify = true;
2690
            }
2691
2692
            return $list;
2693
        }
2694
    }
2695
2696
    /**
2697
     * The link that Google et al. need to index.
2698
     * @return string
2699
     */
2700
    public function CanonicalLink()
2701
    {
2702
        $link = $this->ListAllLink();
2703
        $this->extend('UpdateCanonicalLink', $link);
2704
2705
        return $link;
2706
    }
2707
2708
2709
    /**
2710
     * Link that returns a list of all the products
2711
     * for this product group as a simple list.
2712
     *
2713
     * @return string
2714
     */
2715
    public function ListAllLink()
2716
    {
2717
        if ($this->filterForGroupObject) {
2718
            return $this->Link('filterforgroup/'.$this->filterForGroupObject->URLSegment).'?showfulllist=1';
2719
        } else {
2720
            return $this->Link().'?showfulllist=1';
2721
        }
2722
    }
2723
2724
    /**
2725
     * Link that returns a list of all the products
2726
     * for this product group as a simple list.
2727
     *
2728
     * @return string
2729
     */
2730
    public function ListAFewLink()
2731
    {
2732
        return str_replace('?showfulllist=1', '', $this->ListAllLink());
2733
    }
2734
2735
    /**
2736
     * Link that returns a list of all the products
2737
     * for this product group as a simple list.
2738
     *
2739
     * It resets everything - not just filter....
2740
     *
2741
     * @return string
2742
     */
2743
    public function ResetPreferencesLink($escapedAmpersands = true)
2744
    {
2745
        $ampersand = '&';
2746
        if ($escapedAmpersands) {
2747
            $ampersand = '&amp;';
2748
        }
2749
        $getVariableNameFilter = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2750
        $getVariableNameSort = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2751
2752
        return $this->Link().'?'.
2753
            $getVariableNameFilter.'='.$this->getMyUserPreferencesDefault('FILTER').$ampersand.
2754
            $getVariableNameSort.'='.$this->getMyUserPreferencesDefault('SORT').$ampersand.
2755
            'reload=1';
2756
    }
2757
2758
    /**
2759
     * Link to the search results.
2760
     *
2761
     * @return string
2762
     */
2763
    public function SearchResultLink()
2764
    {
2765
        if ($this->HasSearchResults() && !$this->isSearchResults) {
2766
            return $this->Link('searchresults');
2767
        }
2768
    }
2769
2770
    /****************************************************
2771
     *  INTERNAL PROCESSING: PRODUCT LIST
2772
    /****************************************************/
2773
2774
    /**
2775
     * turns full list into paginated list.
2776
     *
2777
     * @param SS_List
2778
     *
2779
     * @return PaginatedList
0 ignored issues
show
Documentation introduced by
Should the return type not be ProductGroup_Controller|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
2780
     */
2781
    protected function paginateList(SS_List $list)
2782
    {
2783
        if ($list && $list->count()) {
2784
            if ($this->IsShowFullList()) {
2785
                $obj = PaginatedList::create($list, $this->request);
2786
                $obj->setPageLength(EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list') + 1);
2787
2788
                return $obj;
2789
            } else {
2790
                $obj = PaginatedList::create($list, $this->request);
2791
                $obj->setPageLength($this->MyNumberOfProductsPerPage());
2792
2793
                return $obj;
2794
            }
2795
        }
2796
    }
2797
2798
    /****************************************************
2799
     *  INTERNAL PROCESSING: USER PREFERENCES
2800
    /****************************************************/
2801
2802
    /**
2803
     * Checks out a bunch of $_GET variables
2804
     * that are used to work out user preferences
2805
     * Some of these are saved to session.
2806
     *
2807
     * @param array $overrideArray - override $_GET variable settings
2808
     */
2809
    protected function saveUserPreferences($overrideArray = array())
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
2810
    {
2811
        //save sort - filter - display
2812
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2813
        foreach ($sortFilterDisplayNames as $type => $oneTypeArray) {
2814
            $getVariableName = $oneTypeArray['getVariable'];
2815
            $sessionName = $oneTypeArray['sessionName'];
2816
            if (isset($overrideArray[$getVariableName])) {
2817
                $newPreference = $overrideArray[$getVariableName];
2818
            } else {
2819
                $newPreference = $this->request->getVar($getVariableName);
2820
            }
2821
            if ($newPreference) {
2822
                $optionsVariableName = $oneTypeArray['configName'];
2823
                $options = EcommerceConfig::get($this->ClassName, $optionsVariableName);
2824
                if (isset($options[$newPreference])) {
2825
                    Session::set('ProductGroup_'.$sessionName, $newPreference);
2826
                    //save in model as well...
2827
                }
2828
            } else {
2829
                $newPreference = Session::get('ProductGroup_'.$sessionName);
2830
            }
2831
            //save data in model...
2832
            if($newPreference) {
2833
                $this->setCurrentUserPreference($type, $newPreference);
2834
            }
2835
        }
2836
        /* save URLSegments in model
2837
        $this->setCurrentUserPreference(
2838
            "URLSegments",
2839
            array(
2840
                "Action" => $this->request->param("Action"),
2841
                "ID" => $this->request->param("ID")
2842
            )
2843
        );
2844
        */
2845
2846
        //clearing data..
2847
        if ($this->request->getVar('reload')) {
2848
            //reset other session variables...
2849
            Session::set($this->SearchResultsSessionVariable(false), '');
2850
            Session::set($this->SearchResultsSessionVariable(true), '');
2851
2852
            return $this->redirect($this->Link());
2853
        }
2854
2855
        //full list ....
2856
        if ($this->request->getVar('showfulllist')) {
2857
            $this->showFullList = true;
2858
        }
2859
    }
2860
2861
    /**
2862
     * Checks for the most applicable user preferences for this user:
2863
     * 1. session value
2864
     * 2. getMyUserPreferencesDefault.
2865
     *
2866
     * @param string $type - FILTER | SORT | DISPLAY
2867
     *
2868
     * @return string
2869
     *
2870
     * @todo: move to controller?
2871
     */
2872
    protected function getCurrentUserPreferences($type)
2873
    {
2874
        $sessionName = $this->getSortFilterDisplayNames($type, 'sessionName');
2875
        if ($sessionValue = Session::get('ProductGroup_'.$sessionName)) {
2876
            $key = Convert::raw2sql($sessionValue);
2877
        } else {
2878
            $key = $this->getMyUserPreferencesDefault($type);
2879
        }
2880
        $key = $this->getBestKeyAndValidateKey($type, $key);
2881
2882
        return $key;
2883
    }
2884
2885
    /**
2886
     * Provides a dataset of links for a particular user preference.
2887
     *
2888
     * @param string $type SORT | FILTER | DISPLAY - e.g. sort_options
2889
     *
2890
     * @return ArrayList( ArrayData(Name, Link,  SelectKey, Current (boolean), LinkingMode))
0 ignored issues
show
Documentation introduced by
The doc-type ArrayList( could not be parsed: Expected "|" or "end of type", but got "(" at position 9. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
2891
     */
2892
    protected function userPreferencesLinks($type)
2893
    {
2894
        //get basics
2895
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2896
        $options = $this->getConfigOptions($type);
2897
2898
        //if there is only one option then do not bother
2899
        if (count($options) < 2) {
2900
            return;
2901
        }
2902
2903
        //get more config names
2904
        $translationCode = $sortFilterDisplayNames[$type]['translationCode'];
2905
        $getVariableName = $sortFilterDisplayNames[$type]['getVariable'];
2906
        $arrayList = ArrayList::create();
2907
        if (count($options)) {
2908
            foreach ($options as $key => $array) {
2909
                //$isCurrent = ($key == $selectedItem) ? true : false;
2910
2911
                $link = '?'.$getVariableName."=$key";
2912
                if ($type == 'FILTER') {
2913
                    $link = $this->Link().$link;
2914
                } else {
2915
                    $link = $this->request->getVar('url').$link;
2916
                }
2917
                $arrayList->push(ArrayData::create(array(
2918
                    'Name' => _t('ProductGroup.'.$translationCode.strtoupper(str_replace(' ', '', $array['Title'])), $array['Title']),
2919
                    'Link' => $link,
2920
                    'SelectKey' => $key,
2921
                    //we add current at runtime, so we can store the object without current set...
2922
                    //'Current' => $isCurrent,
2923
                    //'LinkingMode' => $isCurrent ? "current" : "link"
2924
                )));
2925
            }
2926
        }
2927
2928
        return $arrayList;
2929
    }
2930
2931
    /****************************************************
2932
     *  INTERNAL PROCESSING: TITLES
2933
    /****************************************************/
2934
2935
    /**
2936
     * variable to make sure secondary title only gets
2937
     * added once.
2938
     *
2939
     * @var bool
2940
     */
2941
    protected $secondaryTitleHasBeenAdded = false;
2942
2943
    /**
2944
     * add a secondary title to the main title
2945
     * in case there is, for example, a filter applied
2946
     * e.g. Socks | MyBrand.
2947
     *
2948
     * @param string
2949
     */
2950
    protected function addSecondaryTitle($secondaryTitle = '')
2951
    {
2952
        $pipe = _t('ProductGroup.TITLE_SEPARATOR', ' | ');
2953
        if (! $this->secondaryTitleHasBeenAdded) {
2954
            if (trim($secondaryTitle)) {
2955
                $secondaryTitle = $pipe.$secondaryTitle;
2956
            }
2957
            if ($this->IsSearchResults()) {
2958
                if ($array = $this->searchResultsArrayFromSession()) {
2959
                    //we remove 1 item here, because the array starts with 0 => 0
2960
                    $count = count($array) - 1;
2961
                    if ($count > 3) {
2962
                        if($this->isSearchResults()) {
2963
                            $canDo = $count < EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list_for_search') ? true : false;
2964
                        } else {
2965
                            $canDo = $count < EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list') ? true : false;
2966
                        }
2967
                        if($canDo) {
2968
                            $toAdd = $count. ' '._t('ProductGroup.PRODUCTS_FOUND', 'Products Found');
2969
                            $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2970
                        }
2971
                    }
2972
                } else {
2973
                    $toAdd = _t('ProductGroup.SEARCH_RESULTS', 'Search Results');
2974
                    $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2975
                }
2976
            }
2977
            if (is_object($this->filterForGroupObject)) {
2978
                $toAdd = $this->filterForGroupObject->Title;
2979
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2980
            }
2981
            $pagination = true;
2982
            if ($this->IsShowFullList()) {
2983
                $toAdd = _t('ProductGroup.LIST_VIEW', 'List View');
2984
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2985
                $pagination = false;
2986
            }
2987
            $filter = $this->getCurrentUserPreferences('FILTER');
2988
            if ($filter != $this->getMyUserPreferencesDefault('FILTER')) {
2989
                $toAdd = $this->getUserPreferencesTitle('FILTER', $this->getCurrentUserPreferences('FILTER'));
2990
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2991
            }
2992
            if ($this->HasSort()) {
2993
                $toAdd = $this->getUserPreferencesTitle('SORT', $this->getCurrentUserPreferences('SORT'));
2994
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2995
            }
2996
            if ($pagination) {
2997
                if ($pageStart = intval($this->request->getVar('start'))) {
2998
                    if ($pageStart > 0) {
2999
                        $page = ($pageStart / $this->MyNumberOfProductsPerPage()) + 1;
3000
                        $toAdd = _t('ProductGroup.PAGE', 'Page') . ' '.$page;
3001
                        $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
3002
                    }
3003
                }
3004
            }
3005
            if ($secondaryTitle) {
3006
                $this->Title .= $secondaryTitle;
3007
                if (isset($this->MetaTitle)) {
3008
                    $this->MetaTitle .= $secondaryTitle;
3009
                }
3010
                if (isset($this->MetaDescription)) {
3011
                    $this->MetaDescription .= $secondaryTitle;
3012
                }
3013
            }
3014
            //dont update menu title, because the entry in the menu
3015
            //should stay the same as it links back to the unfiltered
3016
            //page (in some cases).
3017
3018
            $this->secondaryTitleHasBeenAdded = true;
3019
        }
3020
    }
3021
3022
    /**
3023
     * removes any spaces from the 'toAdd' bit and adds the pipe if there is
3024
     * anything to add at all.  Through the lang files, you can change the pipe
3025
     * symbol to anything you like.
3026
     *
3027
     * @param  string $pipe
3028
     * @param  string $toAdd
3029
     * @return string
3030
     */
3031
    protected function cleanSecondaryTitleForAddition($pipe, $toAdd)
3032
    {
3033
        $toAdd = trim($toAdd);
3034
        $length = strlen($toAdd);
3035
        if ($length > 0) {
3036
            $toAdd = $pipe.$toAdd;
3037
        }
3038
        return $toAdd;
3039
    }
3040
3041
    /****************************************************
3042
     *  DEBUG
3043
    /****************************************************/
3044
3045
    public function debug()
3046
    {
3047
        $member = Member::currentUser();
3048
        if (!$member || !$member->IsShopAdmin()) {
3049
            $messages = array(
3050
                'default' => 'You must login as an admin to use debug functions.',
3051
            );
3052
            Security::permissionFailure($this, $messages);
3053
        }
3054
        $this->ProductsShowable();
3055
        $html = EcommerceTaskDebugCart::debug_object($this->dataRecord);
3056
        $html .= '<ul>';
3057
3058
        $html .= '<li><hr /><h3>Available options</h3><hr /></li>';
3059
        $html .= '<li><b>Sort Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('SORT'), 1).'</pre> </li>';
3060
        $html .= '<li><b>Filter Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('FILTER'), 1).'</pre></li>';
3061
        $html .= '<li><b>Display Styles for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('DISPLAY'), 1).'</pre> </li>';
3062
3063
        $html .= '<li><hr /><h3>Selection Setting (what is set as default for this page)</h3><hr /></li>';
3064
        $html .= '<li><b>MyDefaultFilter:</b> '.$this->getMyUserPreferencesDefault('FILTER').' </li>';
3065
        $html .= '<li><b>MyDefaultSortOrder:</b> '.$this->getMyUserPreferencesDefault('SORT').' </li>';
3066
        $html .= '<li><b>MyDefaultDisplayStyle:</b> '.$this->getMyUserPreferencesDefault('DISPLAY').' </li>';
3067
        $html .= '<li><b>MyNumberOfProductsPerPage:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
3068
        $html .= '<li><b>MyLevelOfProductsToshow:</b> '.$this->MyLevelOfProductsToShow().' = '.(isset($this->showProductLevels[$this->MyLevelOfProductsToShow()]) ? $this->showProductLevels[$this->MyLevelOfProductsToShow()] : 'ERROR!!!! $this->showProductLevels not set for '.$this->MyLevelOfProductsToShow()).' </li>';
3069
3070
        $html .= '<li><hr /><h3>Current Settings</h3><hr /></li>';
3071
        $html .= '<li><b>Current Sort Order:</b> '.$this->getCurrentUserPreferences('SORT').' </li>';
3072
        $html .= '<li><b>Current Filter:</b> '.$this->getCurrentUserPreferences('FILTER').' </li>';
3073
        $html .= '<li><b>Current display style:</b> '.$this->getCurrentUserPreferences('DISPLAY').' </li>';
3074
3075
        $html .= '<li><hr /><h3>DATALIST: totals, numbers per page etc</h3><hr /></li>';
3076
        $html .= '<li><b>Total number of products:</b> '.$this->TotalCount().' </li>';
3077
        $html .= '<li><b>Is there more than one product:</b> '.($this->TotalCountGreaterThanOne() ? 'YES' : 'NO').' </li>';
3078
        $html .= '<li><b>Number of products per page:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
3079
3080
        $html .= '<li><hr /><h3>SQL Factors</h3><hr /></li>';
3081
        $html .= '<li><b>Default sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT'), 1).' </li>';
3082
        $html .= '<li><b>User sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT', $this->getCurrentUserPreferences('SORT')), 1).' </li>';
3083
        $html .= '<li><b>Default Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER'), 1).'</pre> </li>';
3084
        $html .= '<li><b>User Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER', $this->getCurrentUserPreferences('FILTER')), 1).'</pre> </li>';
3085
        $html .= '<li><b>Buyable Class name:</b> '.$this->getBuyableClassName().' </li>';
3086
        $html .= '<li><b>allProducts:</b> '.print_r(str_replace('"', '`', $this->allProducts->sql()), 1).' </li>';
3087
3088
        $html .= '<li><hr /><h3>Search</h3><hr /></li>';
3089
        $resultArray = $this->searchResultsArrayFromSession();
3090
        $productGroupArray = explode(',', Session::get($this->SearchResultsSessionVariable(true)));
3091
        $html .= '<li><b>Is Search Results:</b> '.($this->IsSearchResults() ? 'YES' : 'NO').' </li>';
3092
        $html .= '<li><b>Products In Search (session variable : '.$this->SearchResultsSessionVariable(false).'):</b> '.print_r($resultArray, 1).' </li>';
3093
        $html .= '<li><b>Product Groups In Search (session variable : '.$this->SearchResultsSessionVariable(true).'):</b> '.print_r($productGroupArray, 1).' </li>';
3094
3095
        $html .= '<li><hr /><h3>Other</h3><hr /></li>';
3096
        if ($image = $this->BestAvailableImage()) {
3097
            $html .= '<li><b>Best Available Image:</b> <img src="'.$image->Link.'" /> </li>';
3098
        }
3099
        $html .= '<li><b>BestAvailableImage:</b> '.($this->BestAvailableImage() ? $this->BestAvailableImage()->Link : 'no image available').' </li>';
3100
        $html .= '<li><b>Is this an ecommerce page:</b> '.($this->IsEcommercePage() ? 'YES' : 'NO').' </li>';
3101
        $html .= '<li><hr /><h3>Related Groups</h3><hr /></li>';
3102
        $html .= '<li><b>Parent product group:</b> '.($this->ParentGroup() ? $this->ParentGroup()->Title : '[NO PARENT GROUP]').'</li>';
3103
3104
        $childGroups = $this->ChildGroups(99);
3105
        if ($childGroups->count()) {
3106
            $childGroups = $childGroups->map('ID', 'MenuTitle');
3107
            $html .= '<li><b>Child Groups (all):</b><pre> '.print_r($childGroups, 1).' </pre></li>';
3108
        } else {
3109
            $html .= '<li><b>Child Groups (full tree): </b>NONE</li>';
3110
        }
3111
        $html .= '<li><b>a list of Product Groups that have the products for the CURRENT product group listed as part of their AlsoShowProducts list:</b><pre>'.print_r($this->ProductGroupsFromAlsoShowProducts()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
3112
        $html .= '<li><b>the inverse of ProductGroupsFromAlsoShowProducts:</b><pre> '.print_r($this->ProductGroupsFromAlsoShowProductsInverse()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
3113
        $html .= '<li><b>all product parent groups:</b><pre> '.print_r($this->ProductGroupsParentGroups()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
3114
3115
        $html .= '<li><hr /><h3>Product Example and Links</h3><hr /></li>';
3116
        $product = DataObject::get_one(
3117
            'Product',
3118
            array('ParentID' => $this->ID)
3119
        );
3120
        if ($product) {
3121
            $html .= '<li><b>Product View:</b> <a href="'.$product->Link().'">'.$product->Title.'</a> </li>';
3122
            $html .= '<li><b>Product Debug:</b> <a href="'.$product->Link('debug').'">'.$product->Title.'</a> </li>';
3123
            $html .= '<li><b>Product Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$product->ID.'">'.$product->Title.'</a> </li>';
3124
            $html .= '<li><b>ProductGroup Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$this->ID.'">'.$this->Title.'</a> </li>';
3125
        } else {
3126
            $html .= '<li>this page has no products of its own</li>';
3127
        }
3128
        $html .= '</ul>';
3129
3130
        return $html;
3131
    }
3132
}
3133