Completed
Push — master ( 98705f...2360dd )
by
unknown
03:23
created

ProductGroup::setProductBase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
rs 9.4285
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
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

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

namespace YourVendor;

class YourClass { }

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

Loading history...
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 ...
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
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: SQL
509
     *********************/
510
511
    /**
512
     * SORT:
513
     * Returns the sort sql for a particular sorting key.
514
     * If no key is provided then the default key will be returned.
515
     *
516
     * @param string $key
517
     *
518
     * @return array (e.g. Array(MyField => "ASC", "MyOtherField" => "DESC")
519
     *
520
     * FILTER:
521
     * Returns the sql associated with a filter option.
522
     *
523
     * @param string $type - FILTER | SORT | DISPLAY
524
     * @param string $key  - the options selected
525
     *
526
     * @return array | String (e.g. array("MyField" => 1, "MyOtherField" => 0)) OR STRING
527
     */
528
    protected function getUserSettingsOptionSQL($type, $key = '')
529
    {
530
        $options = $this->getConfigOptions($type);
531
        //if we cant find the current one, use the default
532
        if (!$key || (!isset($options[$key]))) {
533
            $key = $this->getMyUserPreferencesDefault($type);
534
        }
535
        if ($key) {
536
            return $options[$key]['SQL'];
537
        } else {
538
            if ($type == 'FILTER') {
539
                return array('Sort' => 'ASC');
540
            } elseif ($type == 'SORT') {
541
                return array('ShowInSearch' => 1);
542
            }
543
        }
544
    }
545
546
    /*********************
547
     * SETTINGS: Title
548
     *********************/
549
550
    /**
551
     * Returns the Title for a type key.
552
     * If no key is provided then the default key is used.
553
     *
554
     * @param string $type - FILTER | SORT | DISPLAY
555
     * @param string $key
556
     *
557
     * @return string
558
     */
559
    public function getUserPreferencesTitle($type, $key = '')
560
    {
561
        $options = $this->getConfigOptions($type);
562
        if (!$key || (!isset($options[$key]))) {
563
            $key = $this->getMyUserPreferencesDefault($type);
564
        }
565
        if ($key && isset($options[$key]['Title'])) {
566
            return $options[$key]['Title'];
567
        } else {
568
            return _t('ProductGroup.UNKNOWN', 'UNKNOWN USER SETTING');
569
        }
570
    }
571
572
    /*********************
573
     * SETTINGS: products per page
574
     *********************/
575
576
    /**
577
     * @return int
578
     **/
579
    public function ProductsPerPage()
580
    {
581
        return $this->MyNumberOfProductsPerPage();
582
    }
583
584
    private $_numberOfProductsPerPage = null;
585
586
    /**
587
     * @return int
588
     **/
589
    public function MyNumberOfProductsPerPage()
590
    {
591
        if ($this->_numberOfProductsPerPage === null) {
592
            $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...
593
            if ($this->NumberOfProductsPerPage) {
594
                $productsPagePage = $this->NumberOfProductsPerPage;
595
            } else {
596
                if ($parent = $this->ParentGroup()) {
597
                    $productsPagePage = $parent->MyNumberOfProductsPerPage();
598
                } else {
599
                    $productsPagePage = $this->EcomConfig()->NumberOfProductsPerPage;
600
                }
601
            }
602
            $this->_numberOfProductsPerPage = $productsPagePage;
603
        }
604
        return $this->_numberOfProductsPerPage;
605
    }
606
607
    /*********************
608
     * SETTINGS: level of products to show
609
     *********************/
610
611
    /**
612
     * returns the number of product groups (children)
613
     * to show in the current product list
614
     * based on the user setting for this page.
615
     *
616
     * @return int
617
     */
618
    public function MyLevelOfProductsToShow()
619
    {
620
        if ($this->LevelOfProductsToShow == 0) {
621
            if ($parent = $this->ParentGroup()) {
622
                $this->LevelOfProductsToShow = $parent->MyLevelOfProductsToShow();
623
            }
624
        }
625
        //reset to default
626
        if ($this->LevelOfProductsToShow     == 0) {
627
            $defaults = Config::inst()->get('ProductGroup', 'defaults');
628
629
            return isset($defaults['LevelOfProductsToShow']) ? $defaults['LevelOfProductsToShow'] : 99;
630
        }
631
632
        return $this->LevelOfProductsToShow;
633
    }
634
635
    /*********************
636
     * CMS Fields
637
     *********************/
638
639
    /**
640
     * standard SS method.
641
     *
642
     * @return FieldList
643
     */
644
    public function getCMSFields()
645
    {
646
        $fields = parent::getCMSFields();
647
        //dirty hack to show images!
648
        $fields->addFieldToTab('Root.Images', Product_ProductImageUploadField::create('Image', _t('Product.IMAGE', 'Product Group Image')));
649
        //number of products
650
        $calculatedNumberOfProductsPerPage = $this->MyNumberOfProductsPerPage();
651
        $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)') : '';
652
        $fields->addFieldToTab(
653
            'Root',
654
            Tab::create(
655
                'ProductDisplay',
656
                _t('ProductGroup.DISPLAY', 'Display'),
657
                $productsToShowField = DropdownField::create('LevelOfProductsToShow', _t('ProductGroup.PRODUCTSTOSHOW', 'Products to show'), $this->showProductLevels),
658
                HeaderField::create('WhatProductsAreShown', _t('ProductGroup.WHATPRODUCTSSHOWN', _t('ProductGroup.OPTIONSSELECTEDBELOWAPPLYTOCHILDGROUPS', 'Inherited options'))),
659
                $numberOfProductsPerPageField = NumericField::create('NumberOfProductsPerPage', _t('ProductGroup.PRODUCTSPERPAGE', 'Number of products per page'))
660
            )
661
        );
662
        $numberOfProductsPerPageField->setRightTitle($numberOfProductsPerPageExplanation);
663
        if ($calculatedNumberOfProductsPerPage && !$this->NumberOfProductsPerPage) {
664
            $this->NumberOfProductsPerPage = null;
665
            $numberOfProductsPerPageField->setAttribute('placeholder', $calculatedNumberOfProductsPerPage);
666
        }
667
        //sort
668
        $sortDropdownList = $this->getUserPreferencesOptionsForDropdown('SORT');
669
        if (count($sortDropdownList) > 1) {
670
            $sortOrderKey = $this->getMyUserPreferencesDefault('SORT');
671
            if ($this->DefaultSortOrder == 'inherit') {
672
                $actualValue = ' ('.(isset($sortDropdownList[$sortOrderKey]) ? $sortDropdownList[$sortOrderKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
673
                $sortDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
674
            }
675
            $fields->addFieldToTab(
676
                'Root.ProductDisplay',
677
                $defaultSortOrderField = DropdownField::create('DefaultSortOrder', _t('ProductGroup.DEFAULTSORTORDER', 'Default Sort Order'), $sortDropdownList)
678
            );
679
            $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."));
680
        }
681
        //filter
682
        $filterDropdownList = $this->getUserPreferencesOptionsForDropdown('FILTER');
683
        if (count($filterDropdownList) > 1) {
684
            $filterKey = $this->getMyUserPreferencesDefault('FILTER');
685
            if ($this->DefaultFilter == 'inherit') {
686
                $actualValue = ' ('.(isset($filterDropdownList[$filterKey]) ? $filterDropdownList[$filterKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
687
                $filterDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
688
            }
689
            $fields->addFieldToTab(
690
                'Root.ProductDisplay',
691
                $defaultFilterField = DropdownField::create('DefaultFilter', _t('ProductGroup.DEFAULTFILTER', 'Default Filter'), $filterDropdownList)
692
            );
693
            $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."));
694
        }
695
        //display style
696
        $displayStyleDropdownList = $this->getUserPreferencesOptionsForDropdown('DISPLAY');
697
        if (count($displayStyleDropdownList) > 2) {
698
            $displayStyleKey = $this->getMyUserPreferencesDefault('DISPLAY');
699
            if ($this->DisplayStyle == 'inherit') {
700
                $actualValue = ' ('.(isset($displayStyleDropdownList[$displayStyleKey]) ? $displayStyleDropdownList[$displayStyleKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
701
                $displayStyleDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
702
            }
703
            $fields->addFieldToTab(
704
                'Root.ProductDisplay',
705
                DropdownField::create('DisplayStyle', _t('ProductGroup.DEFAULTDISPLAYSTYLE', 'Default Display Style'), $displayStyleDropdownList)
706
            );
707
        }
708
        if ($this->EcomConfig()->ProductsAlsoInOtherGroups) {
709
            if (!$this instanceof ProductGroupSearchPage) {
710
                $fields->addFieldsToTab(
711
                    'Root.OtherProductsShown',
712
                    array(
713
                        HeaderField::create('ProductGroupsHeader', _t('ProductGroup.OTHERPRODUCTSTOSHOW', 'Other products to show ...')),
714
                        $this->getProductGroupsTable(),
715
                    )
716
                );
717
            }
718
        }
719
720
        return $fields;
721
    }
722
723
    /**
724
     * used if you install lumberjack
725
     * @return string
726
     */
727
    public function getLumberjackTitle()
728
    {
729
        return _t('ProductGroup.BUYABLES', 'Products');
730
    }
731
732
    /**
733
     * add this segment to the end of a Product Group
734
     * link to create a cross-filter between the two categories.
735
     *
736
     * @return string
737
     */
738
    public function FilterForGroupLinkSegment()
739
    {
740
        return 'filterforgroup/'.$this->URLSegment.'/';
741
    }
742
743
    // /**
744
    //  * used if you install lumberjack
745
    //  * @return string
746
    //  */
747
    // public function getLumberjackGridFieldConfig()
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
748
    // {
749
    //     return GridFieldConfig_RelationEditor::create();
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
750
    // }
751
752
    /**
753
     * Used in getCSMFields.
754
     *
755
     * @return GridField
756
     **/
757
    protected function getProductGroupsTable()
758
    {
759
        $gridField = GridField::create(
760
            'AlsoShowProducts',
761
            _t('ProductGroup.OTHER_PRODUCTS_SHOWN_IN_THIS_GROUP', 'Other products shown in this group ...'),
762
            $this->AlsoShowProducts(),
763
            GridFieldBasicPageRelationConfig::create()
764
        );
765
        //make sure edits are done in the right place ...
766
        return $gridField;
767
    }
768
769
    /*****************************************************
770
     *
771
     *
772
     *
773
     * PRODUCTS THAT BELONG WITH THIS PRODUCT GROUP
774
     *
775
     *
776
     *
777
     *****************************************************/
778
779
    /**
780
     * returns the inital (all) products, based on the all the eligible products
781
     * for the page.
782
     *
783
     * This is THE pivotal method that probably changes for classes that
784
     * extend ProductGroup as here you can determine what products or other buyables are shown.
785
     *
786
     * The return from this method will then be sorted to produce the final product list.
787
     *
788
     * There is no sort for the initial retrieval
789
     *
790
     * This method is public so that you can retrieve a list of products for a product group page.
791
     *
792
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
793
     * @param string         $alternativeFilterKey Alternative standard filter to be used.
794
     *
795
     * @return DataList
796
     **/
797
    public function currentInitialProducts($extraFilter = null, $alternativeFilterKey = '')
798
    {
799
800
        //INIT ALLPRODUCTS
801
        $this->setProductBase();
802
803
        // GROUP FILTER (PRODUCTS FOR THIS GROUP)
804
        $this->allProducts = $this->getGroupFilter();
805
806
        // STANDARD FILTER (INCLUDES USER PREFERENCE)
807
        $filterStatement = $this->allowPurchaseWhereStatement();
808
        if ($filterStatement) {
809
            if (is_array($filterStatement)) {
810
                $this->allProducts = $this->allProducts->filter($filterStatement);
811
            } elseif (is_string($filterStatement)) {
812
                $this->allProducts = $this->allProducts->where($filterStatement);
813
            }
814
        }
815
        $this->allProducts = $this->getStandardFilter($alternativeFilterKey);
816
817
        // EXTRA FILTER (ON THE FLY FROM CONTROLLER)
818
        if (is_array($extraFilter) && count($extraFilter)) {
819
            $this->allProducts = $this->allProducts->filter($extraFilter);
820
        } elseif (is_string($extraFilter) && strlen($extraFilter) > 2) {
821
            $this->allProducts = $this->allProducts->where($extraFilter);
822
        }
823
824
        //JOINS
825
        $this->allProducts = $this->getGroupJoin();
826
827
        return $this->allProducts;
828
    }
829
830
    protected function setProductBase()
831
    {
832
        unset($this->allProducts);
833
        $className = $this->getBuyableClassName();
834
        $this->allProducts = $className::get();
835
    }
836
837
    /**
838
     * this method can be used quickly current initial products
839
     * whenever you write:
840
     *  ```php
841
     *   currentInitialProducts->(null, $key)->map("ID", "ID")->toArray();
842
     *  ```
843
     * this is the better replacement.
844
     *
845
     * @param string $filterKey
846
     *
847
     * @return array
848
     */
849
    public function currentInitialProductsAsCachedArray($filterKey)
850
    {
851
        $cacheKey = 'CurrentInitialProductsArray'.$filterKey;
852
        if ($array = $this->retrieveObjectStore($cacheKey)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
853
            //do nothing
854
        } else {
855
            $array = $this->currentInitialProducts(null, $filterKey)->map('ID', 'ID')->toArray();
856
            $this->saveObjectStore($array, $cacheKey);
857
        }
858
859
        return $array;
860
    }
861
862
    /*****************************************************
863
     * DATALIST: adjusters
864
     * these are the methods you want to override in
865
     * any clases that extend ProductGroup
866
     *****************************************************/
867
868
    /**
869
     * Do products occur in more than one group.
870
     *
871
     * @return bool
872
     */
873
    protected function getProductsAlsoInOtherGroups()
874
    {
875
        return $this->EcomConfig()->ProductsAlsoInOtherGroups;
876
    }
877
878
    /**
879
     * Returns the class we are working with.
880
     *
881
     * @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...
882
     */
883
    protected function getBuyableClassName()
884
    {
885
        return EcommerceConfig::get('ProductGroup', 'base_buyable_class');
886
    }
887
888
    /**
889
     * @SEE: important notes at the top of this file / class
890
     *
891
     * IMPORTANT: Adjusts allProducts and returns it...
892
     *
893
     * @return DataList
894
     */
895
    protected function getGroupFilter()
896
    {
897
        $levelToShow = $this->MyLevelOfProductsToShow();
898
        $cacheKey = 'GroupFilter_'.abs(intval($levelToShow + 999));
899
        if ($groupFilter = $this->retrieveObjectStore($cacheKey)) {
900
            $this->allProducts = $this->allProducts->where($groupFilter);
901
        } else {
902
            $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...
903
            $productFilterArray = array();
904
            //special cases
905
            if ($levelToShow < 0) {
906
                //no produts but if LevelOfProductsToShow = -1 then show all
907
                $groupFilter = ' ('.$levelToShow.' = -1) ';
908
            } elseif ($levelToShow > 0) {
909
                $groupIDs = array($this->ID => $this->ID);
910
                $productFilterTemp = $this->getProductsToBeIncludedFromOtherGroups();
911
                $productFilterArray[$productFilterTemp] = $productFilterTemp;
912
                $childGroups = $this->ChildGroups($levelToShow);
913
                if ($childGroups && $childGroups->count()) {
914
                    foreach ($childGroups as $childGroup) {
915
                        $groupIDs[$childGroup->ID] = $childGroup->ID;
916
                        $productFilterTemp = $childGroup->getProductsToBeIncludedFromOtherGroups();
917
                        $productFilterArray[$productFilterTemp] = $productFilterTemp;
918
                    }
919
                }
920
                $groupFilter = ' ( "ParentID" IN ('.implode(',', $groupIDs).') ) '.implode($productFilterArray).' ';
921
            } else {
922
                //fall-back
923
                $groupFilter = '"ParentID" < 0';
924
            }
925
            $this->allProducts = $this->allProducts->where($groupFilter);
926
            $this->saveObjectStore($groupFilter, $cacheKey);
927
        }
928
929
        return $this->allProducts;
930
    }
931
932
    /**
933
     * If products are show in more than one group
934
     * Then this returns a where phrase for any products that are linked to this
935
     * product group.
936
     *
937
     * @return string
938
     */
939
    protected function getProductsToBeIncludedFromOtherGroups()
940
    {
941
        //TO DO: this should actually return
942
        //Product.ID = IN ARRAY(bla bla)
943
        $array = array();
944
        if ($this->getProductsAlsoInOtherGroups()) {
945
            $array = $this->AlsoShowProducts()->map('ID', 'ID')->toArray();
946
        }
947
        if (count($array)) {
948
            return " OR (\"Product\".\"ID\" IN (".implode(',', $array).')) ';
949
        }
950
951
        return '';
952
    }
953
954
    /**
955
     * @SEE: important notes at the top of this class / file for more information!
956
     *
957
     * IMPORTANT: Adjusts allProducts and returns it...
958
     *
959
     * @param string $alternativeFilterKey - filter key to be used... if none is specified then we use the current one.
960
     *
961
     * @return DataList
962
     */
963
    protected function getStandardFilter($alternativeFilterKey = '')
964
    {
965
        if ($alternativeFilterKey) {
966
            $filterKey = $alternativeFilterKey;
967
        } else {
968
            $filterKey = $this->getCurrentUserPreferences('FILTER');
969
        }
970
        $filter = $this->getUserSettingsOptionSQL('FILTER', $filterKey);
971
        if (is_array($filter)) {
972
            $this->allProducts = $this->allProducts->Filter($filter);
973
        } elseif (is_string($filter) && strlen($filter) > 2) {
974
            $this->allProducts = $this->allProducts->Where($filter);
975
        }
976
977
        return $this->allProducts;
978
    }
979
980
    /**
981
     * Join statement for the product groups.
982
     *
983
     * IMPORTANT: Adjusts allProducts and returns it...
984
     *
985
     * @return DataList
986
     */
987
    protected function getGroupJoin()
988
    {
989
        return $this->allProducts;
990
    }
991
992
    /**
993
     * Quick - dirty hack - filter to
994
     * only show relevant products.
995
     *
996
     * @param bool   $asArray
997
     * @param string $table
998
     */
999
    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...
1000
    {
1001
        if ($this->EcomConfig()->OnlyShowProductsThatCanBePurchased) {
1002
            if ($asArray) {
1003
                $allowPurchaseWhereStatement = array('AllowPurchase' => 1);
1004
            } else {
1005
                $allowPurchaseWhereStatement = "\"$table\".\"AllowPurchase\" = 1  ";
1006
            }
1007
1008
            return $allowPurchaseWhereStatement;
1009
        }
1010
    }
1011
1012
    /*****************************************************
1013
     *
1014
     *
1015
     *
1016
     *
1017
     * FINAL PRODUCTS
1018
     *
1019
     *
1020
     *
1021
     *
1022
     *****************************************************/
1023
1024
    /**
1025
     * This is the dataList that contains all the products.
1026
     *
1027
     * @var DataList
1028
     */
1029
    protected $allProducts = null;
1030
1031
    /**
1032
     * a list of relevant buyables that can
1033
     * not be purchased and therefore should be excluded.
1034
     * Should be set to NULL to start with so we know if it has been
1035
     * set yet.
1036
     *
1037
     * @var null | Array (like so: array(1,2,4,5,99))
1038
     */
1039
    private $canNOTbePurchasedArray = null;
1040
1041
    /**
1042
     * a list of relevant buyables that can
1043
     * be purchased.  We keep this so that
1044
     * that we can save to session, etc... for future use.
1045
     * Should be set to NULL to start with so we know if it has been
1046
     * set yet.
1047
     *
1048
     * @var null | Array (like so: array(1,2,4,5,99))
1049
     */
1050
    protected $canBePurchasedArray = null;
1051
1052
    /**
1053
     * returns the total numer of products
1054
     * (before pagination AND before MAX is applie).
1055
     *
1056
     * @return int
1057
     **/
1058
    public function RawCount()
1059
    {
1060
        return $this->rawCount ? $this->rawCount : 0;
1061
    }
1062
1063
    /**
1064
     * returns the total numer of products
1065
     * (before pagination but after MAX is applied).
1066
     *
1067
     * @return int
1068
     **/
1069
    public function TotalCount()
1070
    {
1071
        return $this->totalCount ? $this->totalCount : 0;
1072
    }
1073
1074
    /**
1075
     * this is used to save a list of sorted products
1076
     * so that you can find a previous and a next button, etc...
1077
     *
1078
     * @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...
1079
     */
1080
    public function getProductsThatCanBePurchasedArray()
1081
    {
1082
        return $this->canBePurchasedArray;
1083
    }
1084
1085
    /**
1086
     * Retrieve a set of products, based on the given parameters.
1087
     * This method is usually called by the various controller methods.
1088
     * The extraFilter helps you to select different products,
1089
     * depending on the method used in the controller.
1090
     *
1091
     * Furthermore, extrafilter can take all sorts of variables.
1092
     * This is basically setup like this so that in ProductGroup extensions you
1093
     * can setup all sorts of filters, while still using the ProductsShowable method.
1094
     *
1095
     * The extra filter can be supplied as array (e.g. array("ID" => 12) or array("ID" => array(12,13,45)))
1096
     * or as string. Arrays are used like this $productDataList->filter($array) and
1097
     * strings are used with the where commands $productDataList->where($string).
1098
     *
1099
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
1100
     * @param array | string $alternativeSort      Additional SQL for sorting
1101
     * @param string         $alternativeFilterKey alternative filter key to be used
1102
     *
1103
     * @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...
1104
     */
1105
    public function ProductsShowable($extraFilter = null, $alternativeSort = null, $alternativeFilterKey = '')
1106
    {
1107
1108
        //get original products without sort
1109
        $this->allProducts = $this->currentInitialProducts($extraFilter, $alternativeFilterKey);
1110
1111
        //sort products
1112
        $this->allProducts = $this->currentFinalProducts($alternativeSort);
1113
1114
        return $this->allProducts;
1115
    }
1116
1117
    /**
1118
     * returns the final products, based on the all the eligile products
1119
     * for the page.
1120
     *
1121
     * In the process we also save a list of included products
1122
     * and we sort them.  We also keep a record of the total count.
1123
     *
1124
     * All of the 'current' methods are to support the currentFinalProducts Method.
1125
     *
1126
     * @TODO: cache data for faster access.
1127
     *
1128
     * @param array | string $alternativeSort = Alternative Sort String or array
1129
     *
1130
     * @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...
1131
     **/
1132
    protected function currentFinalProducts($alternativeSort = null)
1133
    {
1134
        if ($this->allProducts) {
1135
1136
            //limit to maximum number of products for speed's sake
1137
            $this->allProducts = $this->sortCurrentFinalProducts($alternativeSort);
1138
            $this->allProducts = $this->limitCurrentFinalProducts();
1139
            $this->allProducts = $this->removeExcludedProductsAndSaveIncludedProducts($this->allProducts);
1140
1141
            return $this->allProducts;
1142
        }
1143
    }
1144
1145
    /**
1146
     * returns the SORT part of the final selection of products.
1147
     *
1148
     * @return DataList (allProducts)
1149
     */
1150
    protected function sortCurrentFinalProducts($alternativeSort)
1151
    {
1152
        if ($alternativeSort) {
1153
            if ($this->IsIDarray($alternativeSort)) {
1154
                $sort = $this->createSortStatementFromIDArray($alternativeSort);
1155
            } else {
1156
                $sort = $alternativeSort;
1157
            }
1158
        } else {
1159
            $sort = $this->currentSortSQL();
1160
        }
1161
        $this->allProducts = $this->allProducts->Sort($sort);
1162
1163
        return $this->allProducts;
1164
    }
1165
1166
    /**
1167
     * is the variable provided is an array
1168
     * that can be used as a list of IDs?
1169
     *
1170
     * @param mixed
1171
     *
1172
     * @return bool
1173
     */
1174
    protected function IsIDarray($variable)
1175
    {
1176
        return $variable && is_array($variable) && count($variable) && intval(current($variable)) == current($variable);
1177
    }
1178
1179
    /**
1180
     * returns the SORT part of the final selection of products.
1181
     *
1182
     * @return string | Array
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...
1183
     */
1184
    protected function currentSortSQL()
1185
    {
1186
        $sortKey = $this->getCurrentUserPreferences('SORT');
1187
1188
        return $this->getUserSettingsOptionSQL('SORT', $sortKey);
1189
    }
1190
1191
    /**
1192
     * creates a sort string from a list of ID arrays...
1193
     *
1194
     * @param array $IDarray - list of product IDs
1195
     *
1196
     * @return string
1197
     */
1198
    protected function createSortStatementFromIDArray($IDarray, $table = 'Product')
1199
    {
1200
        $ifStatement = 'CASE ';
1201
        $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...
1202
        $stage = $this->getStage();
1203
        $count = 0;
1204
        foreach ($IDarray as $productID) {
1205
            $ifStatement .= ' WHEN "'.$table.$stage."\".\"ID\" = $productID THEN $count";
1206
            ++$count;
1207
        }
1208
        $sortStatement = $ifStatement.' END';
1209
1210
        return $sortStatement;
1211
    }
1212
1213
    /**
1214
     * limits the products to a maximum number (for speed's sake).
1215
     *
1216
     * @return DataList (this->allProducts adjusted!)
1217
     */
1218
    protected function limitCurrentFinalProducts()
1219
    {
1220
        $this->rawCount = $this->allProducts->count();
1221
        $max = EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list');
1222
        if ($this->rawCount > $max) {
1223
            $this->allProducts = $this->allProducts->limit($max);
1224
            $this->totalCount = $max;
1225
        } else {
1226
            $this->totalCount = $this->rawCount;
1227
        }
1228
1229
        return $this->allProducts;
1230
    }
1231
1232
    /**
1233
     * Excluded products that can not be purchased
1234
     * We all make a record of all the products that are in the current list
1235
     * For efficiency sake, we do both these things at the same time.
1236
     * IMPORTANT: Adjusts allProducts and returns it...
1237
     *
1238
     * @todo: cache data per user ....
1239
     *
1240
     * @return DataList
1241
     */
1242
    protected function removeExcludedProductsAndSaveIncludedProducts()
1243
    {
1244
        if (is_array($this->canBePurchasedArray) && is_array($this->canNOTbePurchasedArray)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1245
            //already done!
1246
        } else {
1247
            $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...
1248
            $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...
1249
            if ($this->config()->get('actively_check_for_can_purchase')) {
1250
                foreach ($this->allProducts as $buyable) {
1251
                    if ($buyable->canPurchase()) {
1252
                        $this->canBePurchasedArray[$buyable->ID] = $buyable->ID;
1253
                    } else {
1254
                        $this->canNOTbePurchasedArray[$buyable->ID] = $buyable->ID;
1255
                    }
1256
                }
1257
            } else {
1258
                if ($this->rawCount > 0) {
1259
                    $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...
1260
                } else {
1261
                    $this->canBePurchasedArray = array();
1262
                }
1263
            }
1264
            if (count($this->canNOTbePurchasedArray)) {
1265
                $this->allProducts = $this->allProducts->Exclude(array('ID' => $this->canNOTbePurchasedArray));
1266
            }
1267
        }
1268
1269
        return $this->allProducts;
1270
    }
1271
1272
    /*****************************************************
1273
     * Children and Parents
1274
     *****************************************************/
1275
1276
    /**
1277
     * Returns children ProductGroup pages of this group.
1278
     *
1279
     * @param int            $maxRecursiveLevel  - maximum depth , e.g. 1 = one level down - so no Child Groups are returned...
1280
     * @param string | Array $filter             - additional filter to be added
1281
     * @param int            $numberOfRecursions - current level of depth
1282
     *
1283
     * @return ArrayList (ProductGroups)
1284
     */
1285
    public function ChildGroups($maxRecursiveLevel, $filter = null, $numberOfRecursions = 0)
1286
    {
1287
        $arrayList = ArrayList::create();
1288
        ++$numberOfRecursions;
1289
        if ($numberOfRecursions < $maxRecursiveLevel) {
1290
            if ($filter && is_string($filter)) {
1291
                $filterWithAND = " AND $filter";
1292
                $where = "\"ParentID\" = '$this->ID' $filterWithAND";
1293
                $children = ProductGroup::get()->where($where);
1294
            } elseif (is_array($filter) && count($filter)) {
1295
                $filter = $filter + array('ParentID' => $this->ID);
1296
                $children = ProductGroup::get()->filter($filter);
1297
            } else {
1298
                $children = ProductGroup::get()->filter(array('ParentID' => $this->ID));
1299
            }
1300
1301
            if ($children->count()) {
1302
                foreach ($children as $child) {
1303
                    $arrayList->push($child);
1304
                    $arrayList->merge($child->ChildGroups($maxRecursiveLevel, $filter, $numberOfRecursions));
1305
                }
1306
            }
1307
        }
1308
        if (! ($arrayList instanceof SS_List)) {
1309
            user_error('We expect an SS_List as output');
1310
        }
1311
1312
        return $arrayList;
1313
    }
1314
1315
    /**
1316
     * Deprecated method.
1317
     */
1318
    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...
1319
    {
1320
        Deprecation::notice('3.1', 'No longer in use');
1321
        if ($maxRecursiveLevel > 24) {
1322
            $maxRecursiveLevel = 24;
1323
        }
1324
1325
        $stage = $this->getStage();
1326
        $select = 'P1.ID as ID1 ';
1327
        $from = "ProductGroup$stage as P1 ";
1328
        $join = " INNER JOIN SiteTree$stage AS S1 ON P1.ID = S1.ID";
1329
        $where = '1 = 1';
1330
        $ids = array(-1);
1331
        for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1332
            $j = $i + 1;
1333
            $select .= ", P$j.ID AS ID$j, S$j.ParentID";
1334
            $join .= "
1335
                LEFT JOIN ProductGroup$stage AS P$j ON P$j.ID = S$i.ParentID
1336
                LEFT JOIN SiteTree$stage AS S$j ON P$j.ID = S$j.ID
1337
            ";
1338
        }
1339
        $rows = DB::Query(' SELECT '.$select.' FROM '.$from.$join.' WHERE '.$where);
1340
        if ($rows) {
1341
            foreach ($rows as $row) {
1342
                for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1343
                    if ($row['ID'.$i]) {
1344
                        $ids[$row['ID'.$i]] = $row['ID'.$i];
1345
                    }
1346
                }
1347
            }
1348
        }
1349
1350
        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...
1351
    }
1352
1353
    /**
1354
     * returns the parent page, but only if it is an instance of Product Group.
1355
     *
1356
     * @return DataObject | Null (ProductGroup)
1357
     **/
1358
    public function ParentGroup()
1359
    {
1360
        if ($this->ParentID) {
1361
            return ProductGroup::get()->byID($this->ParentID);
1362
        }
1363
    }
1364
1365
    /*****************************************************
1366
     * Other Stuff
1367
     *****************************************************/
1368
1369
    /**
1370
     * Recursively generate a product menu.
1371
     *
1372
     * @param string $filter
1373
     *
1374
     * @return ArrayList (ProductGroups)
1375
     */
1376
    public function GroupsMenu($filter = 'ShowInMenus = 1')
1377
    {
1378
        if ($parent = $this->ParentGroup()) {
1379
            return is_a($parent, Object::getCustomClass('ProductGroup')) ? $parent->GroupsMenu() : $this->ChildGroups($filter);
1380
        } else {
1381
            return $this->ChildGroups($filter);
1382
        }
1383
    }
1384
1385
    /**
1386
     * returns a "BestAvailable" image if the current one is not available
1387
     * In some cases this is appropriate and in some cases this is not.
1388
     * For example, consider the following setup
1389
     * - product A with three variations
1390
     * - Product A has an image, but the variations have no images
1391
     * With this scenario, you want to show ONLY the product image
1392
     * on the product page, but if one of the variations is added to the
1393
     * cart, then you want to show the product image.
1394
     * This can be achieved bu using the BestAvailable image.
1395
     *
1396
     * @return Image | Null
1397
     */
1398
    public function BestAvailableImage()
1399
    {
1400
        $image = $this->Image();
1401
        if ($image && $image->exists() && file_exists($image->getFullPath())) {
1402
            return $image;
1403
        } elseif ($parent = $this->ParentGroup()) {
1404
            return $parent->BestAvailableImage();
1405
        }
1406
    }
1407
1408
    /*****************************************************
1409
     * Other related products
1410
     *****************************************************/
1411
1412
    /**
1413
     * returns a list of Product Groups that have the products for
1414
     * the CURRENT product group listed as part of their AlsoShowProducts list.
1415
     *
1416
     * EXAMPLE:
1417
     * You can use the AlsoShowProducts to list products by Brand.
1418
     * In general, they are listed under type product groups (e.g. socks, sweaters, t-shirts),
1419
     * and you create a list of separate ProductGroups (brands) that do not have ANY products as children,
1420
     * but link to products using the AlsoShowProducts many_many relation.
1421
     *
1422
     * With the method below you can work out a list of brands that apply to the
1423
     * current product group (e.g. socks come in three brands - namely A, B and C)
1424
     *
1425
     * @return DataList
1426
     */
1427
    public function ProductGroupsFromAlsoShowProducts()
1428
    {
1429
        $parentIDs = array();
1430
        //we need to add the last array to make sure we have some products...
1431
        $myProductsArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1432
        $rows = array();
1433
        if (count($myProductsArray)) {
1434
            $rows = DB::query('
1435
                SELECT "ProductGroupID"
1436
                FROM "Product_ProductGroups"
1437
                WHERE "ProductID" IN ('.implode(',', $myProductsArray).')
1438
                GROUP BY "ProductGroupID";
1439
            ');
1440
        }
1441
        foreach ($rows as $row) {
1442
            $parentIDs[$row['ProductGroupID']] = $row['ProductGroupID'];
1443
        }
1444
        //just in case
1445
        unset($parentIDs[$this->ID]);
1446
        if (!count($parentIDs)) {
1447
            $parentIDs = array(0 => 0);
1448
        }
1449
1450
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1451
    }
1452
1453
    /**
1454
     * This is the inverse of ProductGroupsFromAlsoShowProducts
1455
     * That is, it list the product groups that a product is primarily listed under (exact parents only)
1456
     * from a "AlsoShow" product List.
1457
     *
1458
     * @return DataList
1459
     */
1460
    public function ProductGroupsFromAlsoShowProductsInverse()
1461
    {
1462
        $alsoShowProductsArray = $this->AlsoShowProducts()
1463
            ->filter($this->getUserSettingsOptionSQL('FILTER', $this->getMyUserPreferencesDefault('FILTER')))
1464
            ->map('ID', 'ID')->toArray();
1465
        $alsoShowProductsArray[0] = 0;
1466
        $parentIDs = Product::get()->filter(array('ID' => $alsoShowProductsArray))->map('ParentID', 'ParentID')->toArray();
1467
        //just in case
1468
        unset($parentIDs[$this->ID]);
1469
        if (! count($parentIDs)) {
1470
            $parentIDs = array(0 => 0);
1471
        }
1472
1473
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInMenus' => 1));
1474
    }
1475
1476
    /**
1477
     * given the products for this page,
1478
     * retrieve the parent groups excluding the current one.
1479
     *
1480
     * @return DataList
1481
     */
1482
    public function ProductGroupsParentGroups()
1483
    {
1484
        $arrayOfIDs = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER')) + array(0 => 0);
1485
        $parentIDs = Product::get()->filter(array('ID' => $arrayOfIDs))->map('ParentID', 'ParentID')->toArray();
1486
        //just in case
1487
        unset($parentIDs[$this->ID]);
1488
        if (! count($parentIDs)) {
1489
            $parentIDs = array(0 => 0);
1490
        }
1491
1492
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1493
    }
1494
1495
    /**
1496
     * returns stage as "" or "_Live".
1497
     *
1498
     * @return string
1499
     */
1500
    protected function getStage()
1501
    {
1502
        $stage = '';
1503
        if (Versioned::current_stage() == 'Live') {
1504
            $stage = '_Live';
1505
        }
1506
1507
        return $stage;
1508
    }
1509
1510
    /*****************************************************
1511
     * STANDARD SS METHODS
1512
     *****************************************************/
1513
1514
    /**
1515
     * tells us if the current page is part of e-commerce.
1516
     *
1517
     * @return bool
1518
     */
1519
    public function IsEcommercePage()
1520
    {
1521
        return true;
1522
    }
1523
1524
    public function onAfterWrite()
1525
    {
1526
        parent::onAfterWrite();
1527
1528
        if ($this->ImageID) {
1529
            if ($normalImage = Image::get()->exclude(array('ClassName' => 'Product_Image'))->byID($this->ImageID)) {
1530
                $normalImage = $normalImage->newClassInstance('Product_Image');
1531
                $normalImage->write();
1532
            }
1533
        }
1534
    }
1535
1536
    public function requireDefaultRecords()
1537
    {
1538
        parent::requireDefaultRecords();
1539
        $urlSegments = ProductGroup::get()->column('URLSegment');
1540
        foreach ($urlSegments as $urlSegment) {
1541
            $counts = array_count_values($urlSegments);
1542
            $hasDuplicates = $counts[$urlSegment]  > 1 ? true : false;
1543
            if ($hasDuplicates) {
1544
                DB::alteration_message('found duplicates for '.$urlSegment, 'deleted');
1545
                $checkForDuplicatesURLSegments = ProductGroup::get()
1546
                    ->filter(array('URLSegment' => $urlSegment));
1547
                if ($checkForDuplicatesURLSegments->count()) {
1548
                    $count = 0;
1549
                    foreach ($checkForDuplicatesURLSegments as $productGroup) {
1550
                        if ($count > 0) {
1551
                            $oldURLSegment = $productGroup->URLSegment;
1552
                            DB::alteration_message(' ... Correcting URLSegment for '.$productGroup->Title.' with ID: '.$productGroup->ID, 'deleted');
1553
                            $productGroup->writeToStage('Stage');
1554
                            $productGroup->publish('Stage', 'Live');
1555
                            $newURLSegment = $productGroup->URLSegment;
1556
                            DB::alteration_message(' ... .... from '.$oldURLSegment.' to '.$newURLSegment, 'created');
1557
                        }
1558
                        $count++;
1559
                    }
1560
                }
1561
            }
1562
        }
1563
    }
1564
1565
    /*****************************************************
1566
     * CACHING
1567
     *****************************************************/
1568
    /**
1569
     *
1570
     * @return bool
1571
     */
1572
    public function AllowCaching()
1573
    {
1574
        return $this->allowCaching;
1575
    }
1576
1577
    /**
1578
     * keeps a cache of the common caching key element
1579
     * @var string
1580
     */
1581
    private static $_product_group_cache_key_cache = null;
1582
1583
    /**
1584
     *
1585
     * @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...
1586
     * @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...
1587
     *
1588
     * @return string
1589
     */
1590
    public function cacheKey($cacheKey)
1591
    {
1592
        $cacheKey = $cacheKey.'_'.$this->ID;
1593
        if (self::$_product_group_cache_key_cache === null) {
1594
            self::$_product_group_cache_key_cache = "_PR_"
1595
                .strtotime(Product::get()->max('LastEdited')). "_"
1596
                .Product::get()->count();
1597
            self::$_product_group_cache_key_cache .= "PG_"
1598
                .strtotime(ProductGroup::get()->max('LastEdited')). "_"
1599
                .ProductGroup::get()->count();
1600
            if (class_exists('ProductVariation')) {
1601
                self::$_product_group_cache_key_cache .= "PV_"
1602
                  .strtotime(ProductVariation::get()->max('LastEdited')). "_"
1603
                  .ProductVariation::get()->count();
1604
            }
1605
        }
1606
        $cacheKey .= self::$_product_group_cache_key_cache;
1607
1608
        return $cacheKey;
1609
    }
1610
1611
    /**
1612
     * @var Zend_Cache_Core
1613
     */
1614
    protected $silverstripeCoreCache = null;
1615
1616
    /**
1617
     * Set the cache object to use when storing / retrieving partial cache blocks.
1618
     *
1619
     * @param Zend_Cache_Core $silverstripeCoreCache
1620
     */
1621
    public function setSilverstripeCoreCache($silverstripeCoreCache)
1622
    {
1623
        $this->silverstripeCoreCache = $silverstripeCoreCache;
1624
    }
1625
1626
    /**
1627
     * Get the cache object to use when storing / retrieving stuff in the Silverstripe Cache
1628
     *
1629
     * @return Zend_Cache_Core
1630
     */
1631
    protected function getSilverstripeCoreCache()
1632
    {
1633
        return $this->silverstripeCoreCache ? $this->silverstripeCoreCache : SS_Cache::factory('EcomPG');
1634
    }
1635
1636
    /**
1637
     * saving an object to the.
1638
     *
1639
     * @param string $cacheKey
1640
     *
1641
     * @return mixed
1642
     */
1643
    protected function retrieveObjectStore($cacheKey)
1644
    {
1645
        $cacheKey = $this->cacheKey($cacheKey);
1646
        if ($this->AllowCaching()) {
1647
            $cache = $this->getSilverstripeCoreCache();
1648
            $data = $cache->load($cacheKey);
1649
            if (!$data) {
1650
                return;
1651
            }
1652
            if (! $cache->getOption('automatic_serialization')) {
1653
                $data = @unserialize($data);
1654
            }
1655
            return $data;
1656
        }
1657
1658
        return;
1659
    }
1660
1661
    /**
1662
     * returns true when the data is saved...
1663
     *
1664
     * @param mixed  $data
1665
     * @param string $cacheKey - key under which the data is saved...
1666
     *
1667
     * @return bool
1668
     */
1669
    protected function saveObjectStore($data, $cacheKey)
1670
    {
1671
        $cacheKey = $this->cacheKey($cacheKey);
1672
        if ($this->AllowCaching()) {
1673
            $cache = $this->getSilverstripeCoreCache();
1674
            if (! $cache->getOption('automatic_serialization')) {
1675
                $data = serialize($data);
1676
            }
1677
            $cache->save($data, $cacheKey);
1678
            return true;
1679
        }
1680
1681
        return false;
1682
    }
1683
1684
    /**
1685
     *
1686
     * @param Boolean $isForGroups OPTIONAL
1687
     *
1688
     * @return string
1689
     */
1690
    public function SearchResultsSessionVariable($isForGroups = false)
1691
    {
1692
        $idString = '_'.$this->ID;
1693
        if ($isForGroups) {
1694
            return Config::inst()->get('ProductSearchForm', 'product_session_variable').$idString;
1695
        } else {
1696
            return Config::inst()->get('ProductSearchForm', 'product_group_session_variable').$idString;
1697
        }
1698
    }
1699
1700
    /**
1701
     * cache for result array.
1702
     *
1703
     * @var array
1704
     */
1705
    private static $_result_array = array();
1706
1707
    /**
1708
     * @return array
1709
     */
1710
    public function searchResultsArrayFromSession()
1711
    {
1712
        if (! isset(self::$_result_array[$this->ID]) || self::$_result_array[$this->ID] === null) {
1713
            self::$_result_array[$this->ID] = explode(',', Session::get($this->SearchResultsSessionVariable(false)));
1714
        }
1715
        if (! is_array(self::$_result_array[$this->ID]) || ! count(self::$_result_array[$this->ID])) {
1716
            self::$_result_array[$this->ID] = array(0 => 0);
1717
        }
1718
1719
        return self::$_result_array[$this->ID];
1720
    }
1721
1722
    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...
1723
    {
1724
        return Product::get()->filter(array('ParentID' => $this->ID))->count();
1725
    }
1726
}
1727
1728
class ProductGroup_Controller extends Page_Controller
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

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

namespace YourVendor;

class YourClass { }

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

Loading history...
1729
{
1730
    /**
1731
     * standard SS variable.
1732
     *
1733
     * @var array
1734
     */
1735
    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...
1736
        'debug' => 'ADMIN',
1737
        'filterforgroup' => true,
1738
        'ProductSearchForm' => true,
1739
        'searchresults' => true,
1740
        'resetfilter' => true,
1741
    );
1742
1743
    /**
1744
     * The original Title of this page before filters, etc...
1745
     *
1746
     * @var string
1747
     */
1748
    protected $originalTitle = '';
1749
1750
    /**
1751
     * list of products that are going to be shown.
1752
     *
1753
     * @var DataList
1754
     */
1755
    protected $products = null;
1756
1757
    /**
1758
     * Show all products on one page?
1759
     *
1760
     * @var bool
1761
     */
1762
    protected $showFullList = false;
1763
1764
    /**
1765
     * The group filter that is applied to this page.
1766
     *
1767
     * @var ProductGroup
1768
     */
1769
    protected $filterForGroupObject = null;
1770
1771
    /**
1772
     * Is this a product search?
1773
     *
1774
     * @var bool
1775
     */
1776
    protected $isSearchResults = false;
1777
1778
    /**
1779
     * standard SS method.
1780
     */
1781
    public function init()
1782
    {
1783
        parent::init();
1784
        $this->originalTitle = $this->Title;
1785
        Requirements::themedCSS('ProductGroup', 'ecommerce');
1786
        Requirements::themedCSS('ProductGroupPopUp', 'ecommerce');
1787
        Requirements::javascript('ecommerce/javascript/EcomProducts.js');
1788
        //we save data from get variables...
1789
        $this->saveUserPreferences();
1790
    }
1791
1792
    /****************************************************
1793
     *  ACTIONS
1794
    /****************************************************/
1795
1796
    /**
1797
     * standard selection of products.
1798
     */
1799
    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...
1800
    {
1801
        //set the filter and the sort...
1802
        $this->addSecondaryTitle();
1803
        $this->products = $this->paginateList($this->ProductsShowable(null));
1804
        if ($this->returnAjaxifiedProductList()) {
1805
            return $this->renderWith('AjaxProductList');
1806
        }
1807
        return array();
1808
    }
1809
1810
    /**
1811
     * cross filter with another product group..
1812
     *
1813
     * e.g. socks (current product group) for brand A or B (the secondary product group)
1814
     *
1815
     * @param HTTPRequest
1816
     */
1817
    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...
1818
    {
1819
        $this->resetfilter();
1820
        $otherGroupURLSegment = Convert::raw2sql($request->param('ID'));
1821
        $arrayOfIDs = array(0 => 0);
1822
        if ($otherGroupURLSegment) {
1823
            $otherProductGroup = DataObject::get_one(
1824
                'ProductGroup',
1825
                array('URLSegment' => $otherGroupURLSegment)
1826
            );
1827
            if ($otherProductGroup) {
1828
                $this->filterForGroupObject = $otherProductGroup;
1829
                $arrayOfIDs = $otherProductGroup->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1830
            }
1831
        }
1832
        $this->addSecondaryTitle();
1833
        $this->products = $this->paginateList($this->ProductsShowable(array('ID' => $arrayOfIDs)));
1834
        if ($this->returnAjaxifiedProductList()) {
1835
            return $this->renderWith('AjaxProductList');
1836
        }
1837
1838
        return array();
1839
    }
1840
1841
1842
    /**
1843
     * get the search results.
1844
     *
1845
     * @param HTTPRequest
1846
     */
1847
    public function searchresults($request)
1848
    {
1849
        $this->resetfilter();
1850
        $this->isSearchResults = true;
1851
        //reset filter and sort
1852
        $resultArray = $this->searchResultsArrayFromSession();
1853
        if (is_array($resultArray)  && count($resultArray)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1854
            //do nothing
1855
        } else {
1856
            $resultArray = array(0 => 0);
1857
        }
1858
        $title = ProductSearchForm::get_last_search_phrase();
1859
        if ($title) {
1860
            $title = _t('Ecommerce.SEARCH_FOR', 'search for: ').substr($title, 0, 25);
1861
        }
1862
        $this->addSecondaryTitle($title);
1863
        $this->products = $this->paginateList($this->ProductsShowable(array('ID' => $resultArray)));
1864
1865
        return array();
1866
    }
1867
1868
    /**
1869
     * resets the filter only.
1870
     */
1871
    public function resetfilter()
1872
    {
1873
        $defaultKey = $this->getMyUserPreferencesDefault('FILTER');
1874
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
1875
        $this->saveUserPreferences(
1876
            array(
1877
                $filterGetVariable => $defaultKey,
1878
            )
1879
        );
1880
1881
        return array();
1882
    }
1883
1884
    /****************************************************
1885
     *  TEMPLATE METHODS PRODUCTS
1886
    /****************************************************/
1887
1888
    /**
1889
     * Return the products for this group.
1890
     * This is the call that is made from the template...
1891
     * The actual final products being shown.
1892
     *
1893
     * @return PaginatedList
0 ignored issues
show
Documentation introduced by
Should the return type not be DataList?

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...
1894
     **/
1895
    public function Products()
1896
    {
1897
        //IMPORTANT!
1898
        //two universal actions!
1899
        $this->addSecondaryTitle();
1900
        $this->cachingRelatedJavascript();
1901
1902
        //save products to session for later use
1903
        $stringOfIDs = '';
1904
        $array = $this->getProductsThatCanBePurchasedArray();
1905
        if (is_array($array)) {
1906
            $stringOfIDs = implode(',', $array);
1907
        }
1908
        //save list for future use
1909
        Session::set(EcommerceConfig::get('ProductGroup', 'session_name_for_product_array'), $stringOfIDs);
1910
1911
        return $this->products;
1912
    }
1913
1914
    /**
1915
     * you can overload this function of ProductGroup Extensions.
1916
     *
1917
     * @return bool
1918
     */
1919
    protected function returnAjaxifiedProductList()
1920
    {
1921
        return Director::is_ajax() ? true : false;
1922
    }
1923
1924
    /**
1925
     * is the product list cache-able?
1926
     *
1927
     * @return bool
1928
     */
1929
    public function ProductGroupListAreCacheable()
1930
    {
1931
        if ($this->productListsHTMLCanBeCached()) {
1932
            //exception 1
1933
            if ($this->IsSearchResults()) {
1934
                return false;
1935
            }
1936
            //exception 2
1937
            $currentOrder = ShoppingCart::current_order();
1938
            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...
1939
                return false;
1940
            }
1941
            //can be cached...
1942
            return true;
1943
        }
1944
1945
        return false;
1946
    }
1947
1948
    /**
1949
     * is the product list ajaxified.
1950
     *
1951
     * @return bool
1952
     */
1953
    public function ProductGroupListAreAjaxified()
1954
    {
1955
        return $this->IsSearchResults() ? false : true;
1956
    }
1957
1958
    /**
1959
     * Unique caching key for the product list...
1960
     *
1961
     * @return string | Null
1962
     */
1963
    public function ProductGroupListCachingKey()
1964
    {
1965
        if ($this->ProductGroupListAreCacheable()) {
1966
            $displayKey = $this->getCurrentUserPreferences('DISPLAY');
1967
            $filterKey = $this->getCurrentUserPreferences('FILTER');
1968
            $filterForGroupKey = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
1969
            $sortKey = $this->getCurrentUserPreferences('SORT');
1970
            $pageStart = $this->request->getVar('start') ? intval($this->request->getVar('start')) : 0;
1971
            $isFullList = $this->IsShowFullList() ? 'Y' : 'N';
1972
            return $this->cacheKey(
1973
                implode(
1974
                    '_',
1975
                    array(
1976
                        $displayKey,
1977
                        $filterKey,
1978
                        $filterForGroupKey,
1979
                        $sortKey,
1980
                        $pageStart,
1981
                        $isFullList,
1982
                    )
1983
                )
1984
            );
1985
        }
1986
1987
        return;
1988
    }
1989
1990
    /**
1991
     * adds Javascript to the page to make it work when products are cached.
1992
     */
1993
    public function CachingRelatedJavascript()
1994
    {
1995
        if ($this->ProductGroupListAreAjaxified()) {
1996
            Requirements::customScript(
1997
                "
1998
                    if(typeof EcomCartOptions === 'undefined') {
1999
                        var EcomCartOptions = {};
2000
                    }
2001
                    EcomCartOptions.ajaxifyProductList = true;
2002
                    EcomCartOptions.ajaxifiedListHolderSelector = '#".$this->AjaxDefinitions()->ProductListHolderID()."';
2003
                    EcomCartOptions.ajaxifiedListAdjusterSelectors = '.".$this->AjaxDefinitions()->ProductListAjaxifiedLinkClassName()."';
2004
                    EcomCartOptions.hiddenPageTitleID = '#".$this->AjaxDefinitions()->HiddenPageTitleID()."';
2005
                ",
2006
                'cachingRelatedJavascript_AJAXlist'
2007
            );
2008
        } else {
2009
            Requirements::customScript(
2010
                "
2011
                    if(typeof EcomCartOptions === 'undefined') {
2012
                        var EcomCartOptions = {};
2013
                    }
2014
                    EcomCartOptions.ajaxifyProductList = false;
2015
                ",
2016
                'cachingRelatedJavascript_AJAXlist'
2017
            );
2018
        }
2019
        $currentOrder = ShoppingCart::current_order();
2020
        if ($currentOrder->TotalItems(true)) {
2021
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
2022
            $obj = new $responseClass();
2023
            $obj->setIncludeHeaders(false);
2024
            $json = $obj->ReturnCartData();
2025
            Requirements::customScript(
2026
                "
2027
                    if(typeof EcomCartOptions === 'undefined') {
2028
                        var EcomCartOptions = {};
2029
                    }
2030
                    EcomCartOptions.initialData= ".$json.";
2031
                ",
2032
                'cachingRelatedJavascript_JSON'
2033
            );
2034
        }
2035
    }
2036
2037
    /**
2038
     * you can overload this function of ProductGroup Extensions.
2039
     *
2040
     * @return bool
2041
     */
2042
    protected function productListsHTMLCanBeCached()
2043
    {
2044
        return Config::inst()->get('ProductGroup', 'actively_check_for_can_purchase') ? false : true;
2045
    }
2046
2047
    /*****************************************************
2048
     * DATALIST: totals, number per page, etc..
2049
     *****************************************************/
2050
2051
    /**
2052
     * returns the total numer of products (before pagination).
2053
     *
2054
     * @return bool
2055
     **/
2056
    public function TotalCountGreaterThanOne($greaterThan = 1)
2057
    {
2058
        return $this->TotalCount() > $greaterThan;
2059
    }
2060
2061
    /**
2062
     * have the ProductsShowable been limited.
2063
     *
2064
     * @return bool
2065
     **/
2066
    public function TotalCountGreaterThanMax()
2067
    {
2068
        return $this->RawCount() >  $this->TotalCount();
2069
    }
2070
2071
    /****************************************************
2072
     *  TEMPLATE METHODS MENUS AND SIDEBARS
2073
    /****************************************************/
2074
2075
    /**
2076
     * title without additions.
2077
     *
2078
     * @return string
2079
     */
2080
    public function OriginalTitle()
2081
    {
2082
        return $this->originalTitle;
2083
    }
2084
    /**
2085
     * This method can be extended to show products in the side bar.
2086
     */
2087
    public function SidebarProducts()
2088
    {
2089
        return;
2090
    }
2091
2092
    /**
2093
     * returns child product groups for use in
2094
     * 'in this section'. For example the vegetable Product Group
2095
     * May have listed here: Carrot, Cabbage, etc...
2096
     *
2097
     * @return ArrayList (ProductGroups)
2098
     */
2099
    public function MenuChildGroups()
2100
    {
2101
        return $this->ChildGroups(2, '"ShowInMenus" = 1');
2102
    }
2103
2104
    /**
2105
     * After a search is conducted you may end up with a bunch
2106
     * of recommended product groups. They will be returned here...
2107
     * We sort the list in the order that it is provided.
2108
     *
2109
     * @return DataList | Null (ProductGroups)
2110
     */
2111
    public function SearchResultsChildGroups()
2112
    {
2113
        $groupArray = explode(',', Session::get($this->SearchResultsSessionVariable($isForGroup = true)));
2114
        if (is_array($groupArray) && count($groupArray)) {
2115
            $sortStatement = $this->createSortStatementFromIDArray($groupArray, 'ProductGroup');
2116
2117
            return ProductGroup::get()->filter(array('ID' => $groupArray, 'ShowInSearch' => 1))->sort($sortStatement);
2118
        }
2119
2120
        return;
2121
    }
2122
2123
    /****************************************************
2124
     *  Search Form Related controllers
2125
    /****************************************************/
2126
2127
    /**
2128
     * returns a search form to search current products.
2129
     *
2130
     * @return ProductSearchForm object
2131
     */
2132
    public function ProductSearchForm()
2133
    {
2134
        $onlySearchTitle = $this->originalTitle;
2135
        if ($this->dataRecord instanceof ProductGroupSearchPage) {
2136
            if ($this->HasSearchResults()) {
2137
                $onlySearchTitle = 'Last Search Results';
2138
            }
2139
        }
2140
        $form = ProductSearchForm::create(
2141
            $this,
2142
            'ProductSearchForm',
2143
            $onlySearchTitle,
2144
            $this->currentInitialProducts(null, $this->getMyUserPreferencesDefault('FILTER'))
2145
        );
2146
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2147
        $sortGetVariable = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2148
        $additionalGetParameters = $filterGetVariable.'='.$this->getMyUserPreferencesDefault('FILTER').'&'.
2149
                                   $sortGetVariable.'='.$this->getMyUserPreferencesDefault('SORT');
2150
        $form->setAdditionalGetParameters($additionalGetParameters);
2151
2152
        return $form;
2153
    }
2154
2155
    /**
2156
     * Does this page have any search results?
2157
     * If search was carried out without returns
2158
     * then it returns zero (false).
2159
     *
2160
     * @return int | false
2161
     */
2162
    public function HasSearchResults()
2163
    {
2164
        $resultArray = $this->searchResultsArrayFromSession();
2165
        if ($resultArray) {
2166
            $count = count($resultArray) - 1;
2167
2168
            return $count ? $count : 0;
2169
        }
2170
2171
        return 0;
2172
    }
2173
2174
    /**
2175
     * Should the product search form be shown immediately?
2176
     *
2177
     * @return bool
2178
     */
2179
    public function ShowSearchFormImmediately()
2180
    {
2181
        if ($this->IsSearchResults()) {
2182
            return true;
2183
        }
2184
        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...
2185
            return false;
2186
        }
2187
2188
        return true;
2189
    }
2190
2191
    /**
2192
     * Show a search form on this page?
2193
     *
2194
     * @return bool
2195
     */
2196
    public function ShowSearchFormAtAll()
2197
    {
2198
        return true;
2199
    }
2200
2201
    /**
2202
     * Is the current page a display of search results.
2203
     *
2204
     * This does not mean that something is actively being search for,
2205
     * it could also be just "showing the search results"
2206
     *
2207
     * @return bool
2208
     */
2209
    public function IsSearchResults()
2210
    {
2211
        return $this->isSearchResults;
2212
    }
2213
2214
    /**
2215
     * Is there something actively being searched for?
2216
     *
2217
     * This is different from IsSearchResults.
2218
     *
2219
     * @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...
2220
     */
2221
    public function ActiveSearchTerm()
2222
    {
2223
        $data = Session::get(Config::inst()->get('ProductSearchForm', 'form_data_session_variable'));
2224
        if (!empty($data['Keyword'])) {
2225
            return $this->IsSearchResults();
2226
        }
2227
    }
2228
2229
    /****************************************************
2230
     *  Filter / Sort / Display related controllers
2231
    /****************************************************/
2232
2233
    /**
2234
     * Do we show all products on one page?
2235
     *
2236
     * @return bool
2237
     */
2238
    public function ShowFiltersAndDisplayLinks()
2239
    {
2240
        if ($this->TotalCountGreaterThanOne()) {
2241
            if ($this->HasFilters()) {
2242
                return true;
2243
            }
2244
            if ($this->DisplayLinks()) {
2245
                return true;
2246
            }
2247
        }
2248
2249
        return false;
2250
    }
2251
2252
    /**
2253
     * Do we show the sort links.
2254
     *
2255
     * A bit arbitrary to say three,
2256
     * but there is not much point to sort three or less products
2257
     *
2258
     * @return bool
2259
     */
2260
    public function ShowSortLinks($minimumCount = 3)
2261
    {
2262
        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...
2263
            return true;
2264
        }
2265
2266
        return false;
2267
    }
2268
2269
    /**
2270
     * Is there a special filter operating at the moment?
2271
     * Is the current filter the default one (return inverse!)?
2272
     *
2273
     * @return bool
2274
     */
2275
    public function HasFilter()
2276
    {
2277
        return $this->getCurrentUserPreferences('FILTER') != $this->getMyUserPreferencesDefault('FILTER')
2278
        || $this->filterForGroupObject;
2279
    }
2280
2281
    /**
2282
     * Is there a special sort operating at the moment?
2283
     * Is the current sort the default one (return inverse!)?
2284
     *
2285
     * @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...
2286
     */
2287
    public function HasSort()
2288
    {
2289
        $sort = $this->getCurrentUserPreferences('SORT');
2290
        if ($sort != $this->getMyUserPreferencesDefault('SORT')) {
2291
            return true;
2292
        }
2293
    }
2294
2295
    /**
2296
     * @return boolean
2297
     */
2298
    public function HasFilterOrSort()
2299
    {
2300
        return $this->HasFilter() || $this->HasSort();
2301
    }
2302
2303
    /**
2304
     * @return boolean
2305
     */
2306
    public function HasFilterOrSortFullList()
2307
    {
2308
        return $this->HasFilterOrSort() || $this->IsShowFullList();
2309
    }
2310
2311
    /**
2312
     * are filters available?
2313
     * we check one at the time so that we do the least
2314
     * amount of DB queries.
2315
     *
2316
     * @return bool
2317
     */
2318
    public function HasFilters()
2319
    {
2320
        $countFilters = $this->FilterLinks()->count();
2321
        if ($countFilters > 1) {
2322
            return true;
2323
        }
2324
        $countGroupFilters = $this->ProductGroupFilterLinks()->count();
2325
        if ($countGroupFilters > 1) {
2326
            return true;
2327
        }
2328
        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...
2329
            return true;
2330
        }
2331
2332
        return false;
2333
    }
2334
2335
    /**
2336
     * Do we show all products on one page?
2337
     *
2338
     * @return bool
2339
     */
2340
    public function IsShowFullList()
2341
    {
2342
        return $this->showFullList;
2343
    }
2344
2345
    /**
2346
     * returns the current filter applied to the list
2347
     * in a human readable string.
2348
     *
2349
     * @return string
2350
     */
2351
    public function CurrentDisplayTitle()
2352
    {
2353
        $displayKey = $this->getCurrentUserPreferences('DISPLAY');
2354
        if ($displayKey != $this->getMyUserPreferencesDefault('DISPLAY')) {
2355
            return $this->getUserPreferencesTitle('DISPLAY', $displayKey);
2356
        }
2357
    }
2358
2359
    /**
2360
     * returns the current filter applied to the list
2361
     * in a human readable string.
2362
     *
2363
     * @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...
2364
     */
2365
    public function CurrentFilterTitle()
2366
    {
2367
        $filterKey = $this->getCurrentUserPreferences('FILTER');
2368
        $filters = array();
2369
        if ($filterKey != $this->getMyUserPreferencesDefault('FILTER')) {
2370
            $filters[] = $this->getUserPreferencesTitle('FILTER', $filterKey);
2371
        }
2372
        if ($this->filterForGroupObject) {
2373
            $filters[] = $this->filterForGroupObject->MenuTitle;
2374
        }
2375
        if (count($filters)) {
2376
            return implode(', ', $filters);
2377
        }
2378
    }
2379
2380
    /**
2381
     * returns the current sort applied to the list
2382
     * in a human readable string.
2383
     *
2384
     * @return string
2385
     */
2386
    public function CurrentSortTitle()
2387
    {
2388
        $sortKey = $this->getCurrentUserPreferences('SORT');
2389
        if ($sortKey != $this->getMyUserPreferencesDefault('SORT')) {
2390
            return $this->getUserPreferencesTitle('SORT', $sortKey);
2391
        }
2392
    }
2393
2394
    /**
2395
     * short-cut for getMyUserPreferencesDefault("DISPLAY")
2396
     * for use in templtes.
2397
     *
2398
     * @return string - key
2399
     */
2400
    public function MyDefaultDisplayStyle()
2401
    {
2402
        return $this->getMyUserPreferencesDefault('DISPLAY');
2403
    }
2404
2405
    /**
2406
     * Number of entries per page limited by total number of pages available...
2407
     *
2408
     * @return int
2409
     */
2410
    public function MaxNumberOfProductsPerPage()
2411
    {
2412
        return $this->MyNumberOfProductsPerPage() > $this->TotalCount() ? $this->TotalCount() : $this->MyNumberOfProductsPerPage();
2413
    }
2414
2415
    /****************************************************
2416
     *  TEMPLATE METHODS FILTER LINK
2417
    /****************************************************/
2418
2419
    /**
2420
     * Provides a ArrayList of links for filters products.
2421
     *
2422
     * @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...
2423
     */
2424
    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...
2425
    {
2426
        $cacheKey = 'FilterLinks_'.($this->filterForGroupObject ? $this->filterForGroupObject->ID : 0);
2427
        if ($list = $this->retrieveObjectStore($cacheKey)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
2428
            //do nothing
2429
        } else {
2430
            $list = $this->userPreferencesLinks('FILTER');
2431
            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...
2432
                $key = $obj->SelectKey;
2433
                if ($key != $this->getMyUserPreferencesDefault('FILTER')) {
2434
                    $count = count($this->currentInitialProductsAsCachedArray($key));
2435
                    if ($count == 0) {
2436
                        $list->remove($obj);
2437
                    } else {
2438
                        $obj->Count = $count;
2439
                    }
2440
                }
2441
            }
2442
            $this->saveObjectStore($list, $cacheKey);
2443
        }
2444
        $selectedItem = $this->getCurrentUserPreferences('FILTER');
2445
        foreach ($list as $obj) {
2446
            $canHaveCurrent = true;
2447
            if ($this->filterForGroupObject) {
2448
                $canHaveCurrent = false;
2449
            }
2450
            $obj->Current = $selectedItem == $obj->SelectKey && $canHaveCurrent ? true : false;
2451
            $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2452
            $obj->Ajaxify = true;
2453
        }
2454
2455
        return $list;
2456
    }
2457
2458
    /**
2459
     * returns a list of items (with links).
2460
     *
2461
     * @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...
2462
     */
2463
    public function ProductGroupFilterLinks()
2464
    {
2465
        if ($array = $this->retrieveObjectStore('ProductGroupFilterLinks')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
2466
            //do nothing
2467
        } else {
2468
            $arrayOfItems = array();
2469
2470
            $baseArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2471
2472
            //also show
2473
            $items = $this->ProductGroupsFromAlsoShowProducts();
2474
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2475
            //also show inverse
2476
            $items = $this->ProductGroupsFromAlsoShowProductsInverse();
2477
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2478
2479
            //parent groups
2480
            $items = $this->ProductGroupsParentGroups();
2481
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2482
2483
            //child groups
2484
            $items = $this->MenuChildGroups();
2485
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2486
2487
            ksort($arrayOfItems);
2488
            $array = array();
2489
            foreach ($arrayOfItems as $arrayOfItem) {
2490
                $array[] = $this->makeArrayItem($arrayOfItem);
2491
            }
2492
            $this->saveObjectStore($array, 'ProductGroupFilterLinks');
2493
        }
2494
        $arrayList = ArrayList::create();
2495
        foreach ($array as $item) {
2496
            $arrayList->push(ArrayData::create($item));
2497
        }
2498
2499
        return $arrayList;
2500
    }
2501
2502
2503
    /**
2504
     * @see ProductGroupFilterLinks
2505
     * same as ProductGroupFilterLinks, but with originating Object...
2506
     *
2507
     * @return ArrayList
2508
     */
2509
    public function ProductGroupFilterOriginalObjects()
2510
    {
2511
        $links = $this->ProductGroupFilterLinks();
2512
        // /print_r($links);
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
2513
        foreach ($links as $linkItem) {
2514
            $className = $linkItem->ClassName;
2515
            $id = $linkItem->ID;
2516
            if ($className && $id) {
2517
                $object = $className::get()->byID($id);
2518
                $linkItem->Object = $object;
2519
            }
2520
        }
2521
2522
2523
        return $links;
2524
    }
2525
2526
    /**
2527
     * counts the total number in the combination....
2528
     *
2529
     * @param DataList $items     - list of
2530
     * @param Arary    $baseArray - list of products on the current page
2531
     *
2532
     * @return array
2533
     */
2534
    protected function productGroupFilterLinksCount($items, $baseArray, $ajaxify = true)
2535
    {
2536
        $array = array();
2537
        if ($items && $items->count()) {
2538
            foreach ($items as $item) {
2539
                $arrayOfIDs = $item->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2540
                $newArray = array_intersect_key(
2541
                    $arrayOfIDs,
2542
                    $baseArray
2543
                );
2544
                $count = count($newArray);
2545
                if ($count) {
2546
                    $array[$item->Title] = array(
2547
                        'Item' => $item,
2548
                        'Count' => $count,
2549
                        'Ajaxify' => $ajaxify,
2550
                    );
2551
                }
2552
            }
2553
        }
2554
2555
        return $array;
2556
    }
2557
2558
    /**
2559
     * @param array itemInArray (Item, Count, UserFilterAction)
2560
     *
2561
     * @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...
2562
     */
2563
    protected function makeArrayItem($itemInArray)
2564
    {
2565
        $item = $itemInArray['Item'];
2566
        $count = $itemInArray['Count'];
2567
        $ajaxify = $itemInArray['Ajaxify'];
2568
        $filterForGroupObjectID = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
2569
        $isCurrent = ($item->ID == $filterForGroupObjectID ? true : false);
2570
        if ($ajaxify) {
2571
            $link = $this->Link($item->FilterForGroupLinkSegment());
2572
        } else {
2573
            $link = $item->Link();
2574
        }
2575
        return array(
2576
            'ID' => $item->ID,
2577
            'ClassName' => $item->ClassName,
2578
            'Title' => $item->Title,
2579
            'Count' => $count,
2580
            'SelectKey' => $item->URLSegment,
2581
            'Current' => $isCurrent ? true : false,
2582
            'MyLinkingMode' => $isCurrent ? 'current' : 'link',
2583
            'FilterLink' => $link,
2584
            'Ajaxify' => $ajaxify ? true : false,
2585
        );
2586
    }
2587
2588
    /**
2589
     * Provides a ArrayList of links for sorting products.
2590
     */
2591
    public function SortLinks()
2592
    {
2593
        $list = $this->userPreferencesLinks('SORT');
2594
        $selectedItem = $this->getCurrentUserPreferences('SORT');
2595
        if ($list) {
2596
            foreach ($list as $obj) {
2597
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2598
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2599
                $obj->Ajaxify = true;
2600
            }
2601
2602
            return $list;
2603
        }
2604
    }
2605
2606
    /**
2607
     * Provides a ArrayList for displaying display links.
2608
     */
2609
    public function DisplayLinks()
2610
    {
2611
        $list = $this->userPreferencesLinks('DISPLAY');
2612
        $selectedItem = $this->getCurrentUserPreferences('DISPLAY');
2613
        if ($list) {
2614
            foreach ($list as $obj) {
2615
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2616
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2617
                $obj->Ajaxify = true;
2618
            }
2619
2620
            return $list;
2621
        }
2622
    }
2623
2624
    /**
2625
     * The link that Google et al. need to index.
2626
     * @return string
2627
     */
2628
    public function CanonicalLink()
2629
    {
2630
        $link = $this->ListAllLink();
2631
        $this->extend('UpdateCanonicalLink', $link);
2632
2633
        return $link;
2634
    }
2635
2636
2637
    /**
2638
     * Link that returns a list of all the products
2639
     * for this product group as a simple list.
2640
     *
2641
     * @return string
2642
     */
2643
    public function ListAllLink()
2644
    {
2645
        if ($this->filterForGroupObject) {
2646
            return $this->Link('filterforgroup/'.$this->filterForGroupObject->URLSegment).'?showfulllist=1';
2647
        } else {
2648
            return $this->Link().'?showfulllist=1';
2649
        }
2650
    }
2651
2652
    /**
2653
     * Link that returns a list of all the products
2654
     * for this product group as a simple list.
2655
     *
2656
     * @return string
2657
     */
2658
    public function ListAFewLink()
2659
    {
2660
        return str_replace('?showfulllist=1', '', $this->ListAllLink());
2661
    }
2662
2663
    /**
2664
     * Link that returns a list of all the products
2665
     * for this product group as a simple list.
2666
     *
2667
     * It resets everything - not just filter....
2668
     *
2669
     * @return string
2670
     */
2671
    public function ResetPreferencesLink($escapedAmpersands = true)
2672
    {
2673
        $ampersand = '&';
2674
        if ($escapedAmpersands) {
2675
            $ampersand = '&amp;';
2676
        }
2677
        $getVariableNameFilter = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2678
        $getVariableNameSort = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2679
2680
        return $this->Link().'?'.
2681
            $getVariableNameFilter.'='.$this->getMyUserPreferencesDefault('FILTER').$ampersand.
2682
            $getVariableNameSort.'='.$this->getMyUserPreferencesDefault('SORT').$ampersand.
2683
            'reload=1';
2684
    }
2685
2686
    /**
2687
     * Link to the search results.
2688
     *
2689
     * @return string
2690
     */
2691
    public function SearchResultLink()
2692
    {
2693
        if ($this->HasSearchResults() && !$this->isSearchResults) {
2694
            return $this->Link('searchresults');
2695
        }
2696
    }
2697
2698
    /****************************************************
2699
     *  INTERNAL PROCESSING: PRODUCT LIST
2700
    /****************************************************/
2701
2702
    /**
2703
     * turns full list into paginated list.
2704
     *
2705
     * @param SS_List
2706
     *
2707
     * @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...
2708
     */
2709
    protected function paginateList(SS_List $list)
2710
    {
2711
        if ($list && $list->count()) {
2712
            if ($this->IsShowFullList()) {
2713
                $obj = PaginatedList::create($list, $this->request);
2714
                $obj->setPageLength(EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list') + 1);
2715
2716
                return $obj;
2717
            } else {
2718
                $obj = PaginatedList::create($list, $this->request);
2719
                $obj->setPageLength($this->MyNumberOfProductsPerPage());
2720
2721
                return $obj;
2722
            }
2723
        }
2724
    }
2725
2726
    /****************************************************
2727
     *  INTERNAL PROCESSING: USER PREFERENCES
2728
    /****************************************************/
2729
2730
    /**
2731
     * Checks out a bunch of $_GET variables
2732
     * that are used to work out user preferences
2733
     * Some of these are saved to session.
2734
     *
2735
     * @param array $overrideArray - override $_GET variable settings
2736
     */
2737
    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...
2738
    {
2739
2740
        //save sort - filter - display
2741
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2742
        foreach ($sortFilterDisplayNames as $type => $oneTypeArray) {
2743
            $getVariableName = $oneTypeArray['getVariable'];
2744
            $sessionName = $oneTypeArray['sessionName'];
2745
            if (isset($overrideArray[$getVariableName])) {
2746
                $newPreference = $overrideArray[$getVariableName];
2747
            } else {
2748
                $newPreference = $this->request->getVar($getVariableName);
2749
            }
2750
            if ($newPreference) {
2751
                $optionsVariableName = $oneTypeArray['configName'];
2752
                $options = EcommerceConfig::get($this->ClassName, $optionsVariableName);
2753
                if (isset($options[$newPreference])) {
2754
                    Session::set('ProductGroup_'.$sessionName, $newPreference);
2755
                    //save in model as well...
2756
                }
2757
            } else {
2758
                $newPreference = Session::get('ProductGroup_'.$sessionName);
2759
            }
2760
            //save data in model...
2761
            $this->setCurrentUserPreference($type, $newPreference);
2762
        }
2763
        /* save URLSegments in model
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
2764
        $this->setCurrentUserPreference(
2765
            "URLSegments",
2766
            array(
2767
                "Action" => $this->request->param("Action"),
2768
                "ID" => $this->request->param("ID")
2769
            )
2770
        );
2771
        */
2772
2773
        //clearing data..
2774
        if ($this->request->getVar('reload')) {
2775
            //reset other session variables...
2776
            Session::set($this->SearchResultsSessionVariable(false), '');
2777
            Session::set($this->SearchResultsSessionVariable(true), '');
2778
2779
            return $this->redirect($this->Link());
2780
        }
2781
2782
        //full list ....
2783
        if ($this->request->getVar('showfulllist')) {
2784
            $this->showFullList = true;
2785
        }
2786
    }
2787
2788
    /**
2789
     * Checks for the most applicable user preferences for this user:
2790
     * 1. session value
2791
     * 2. getMyUserPreferencesDefault.
2792
     *
2793
     * @param string $type - FILTER | SORT | DISPLAY
2794
     *
2795
     * @return string
2796
     *
2797
     * @todo: move to controller?
2798
     */
2799
    protected function getCurrentUserPreferences($type)
2800
    {
2801
        $sessionName = $this->getSortFilterDisplayNames($type, 'sessionName');
2802
        if ($sessionValue = Session::get('ProductGroup_'.$sessionName)) {
2803
            $key = Convert::raw2sql($sessionValue);
2804
        } else {
2805
            $key = $this->getMyUserPreferencesDefault($type);
2806
        }
2807
2808
        return $key;
2809
    }
2810
2811
    /**
2812
     * Provides a dataset of links for a particular user preference.
2813
     *
2814
     * @param string $type SORT | FILTER | DISPLAY - e.g. sort_options
2815
     *
2816
     * @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...
2817
     */
2818
    protected function userPreferencesLinks($type)
2819
    {
2820
        //get basics
2821
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2822
        $options = $this->getConfigOptions($type);
2823
2824
        //if there is only one option then do not bother
2825
        if (count($options) < 2) {
2826
            return;
2827
        }
2828
2829
        //get more config names
2830
        $translationCode = $sortFilterDisplayNames[$type]['translationCode'];
2831
        $getVariableName = $sortFilterDisplayNames[$type]['getVariable'];
2832
        $arrayList = ArrayList::create();
2833
        if (count($options)) {
2834
            foreach ($options as $key => $array) {
2835
                //$isCurrent = ($key == $selectedItem) ? true : false;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
2836
2837
                $link = '?'.$getVariableName."=$key";
2838
                if ($type == 'FILTER') {
2839
                    $link = $this->Link().$link;
2840
                } else {
2841
                    $link = $this->request->getVar('url').$link;
2842
                }
2843
                $arrayList->push(ArrayData::create(array(
2844
                    'Name' => _t('ProductGroup.'.$translationCode.strtoupper(str_replace(' ', '', $array['Title'])), $array['Title']),
2845
                    'Link' => $link,
2846
                    'SelectKey' => $key,
2847
                    //we add current at runtime, so we can store the object without current set...
2848
                    //'Current' => $isCurrent,
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
2849
                    //'LinkingMode' => $isCurrent ? "current" : "link"
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
2850
                )));
2851
            }
2852
        }
2853
2854
        return $arrayList;
2855
    }
2856
2857
    /****************************************************
2858
     *  INTERNAL PROCESSING: TITLES
2859
    /****************************************************/
2860
2861
    /**
2862
     * variable to make sure secondary title only gets
2863
     * added once.
2864
     *
2865
     * @var bool
2866
     */
2867
    protected $secondaryTitleHasBeenAdded = false;
2868
2869
    /**
2870
     * add a secondary title to the main title
2871
     * in case there is, for example, a filter applied
2872
     * e.g. Socks | MyBrand.
2873
     *
2874
     * @param string
2875
     */
2876
    protected function addSecondaryTitle($secondaryTitle = '')
2877
    {
2878
        $pipe = _t('ProductGroup.TITLE_SEPARATOR', ' | ');
2879
        if (! $this->secondaryTitleHasBeenAdded) {
2880
            if (trim($secondaryTitle)) {
2881
                $secondaryTitle = $pipe.$secondaryTitle;
2882
            }
2883
            if ($this->IsSearchResults()) {
2884
                if ($array = $this->searchResultsArrayFromSession()) {
2885
                    //we remove 1 item here, because the array starts with 0 => 0
2886
                    $count = count($array) - 1;
2887
                    if ($count > 3) {
2888
                        $toAdd = $count. ' '._t('ProductGroup.PRODUCTS_FOUND', 'Products Found');
2889
                        $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2890
                    }
2891
                } else {
2892
                    $toAdd = _t('ProductGroup.SEARCH_RESULTS', 'Search Results');
2893
                    $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2894
                }
2895
            }
2896
            if (is_object($this->filterForGroupObject)) {
2897
                $toAdd = $this->filterForGroupObject->Title;
2898
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2899
            }
2900
            $pagination = true;
2901
            if ($this->IsShowFullList()) {
2902
                $toAdd = _t('ProductGroup.LIST_VIEW', 'List View');
2903
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2904
                $pagination = false;
2905
            }
2906
            $filter = $this->getCurrentUserPreferences('FILTER');
2907
            if ($filter != $this->getMyUserPreferencesDefault('FILTER')) {
2908
                $toAdd = $this->getUserPreferencesTitle('FILTER', $this->getCurrentUserPreferences('FILTER'));
2909
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2910
            }
2911
            if ($this->HasSort()) {
2912
                $toAdd = $this->getUserPreferencesTitle('SORT', $this->getCurrentUserPreferences('SORT'));
2913
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2914
            }
2915
            if ($pagination) {
2916
                if ($pageStart = intval($this->request->getVar('start'))) {
2917
                    if ($pageStart > 0) {
2918
                        $page = ($pageStart / $this->MyNumberOfProductsPerPage()) + 1;
2919
                        $toAdd = _t('ProductGroup.PAGE', 'Page') . ' '.$page;
2920
                        $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2921
                    }
2922
                }
2923
            }
2924
            if ($secondaryTitle) {
2925
                $this->Title .= $secondaryTitle;
2926
                if (isset($this->MetaTitle)) {
2927
                    $this->MetaTitle .= $secondaryTitle;
2928
                }
2929
                if (isset($this->MetaDescription)) {
2930
                    $this->MetaDescription .= $secondaryTitle;
2931
                }
2932
            }
2933
            //dont update menu title, because the entry in the menu
2934
            //should stay the same as it links back to the unfiltered
2935
            //page (in some cases).
2936
2937
            $this->secondaryTitleHasBeenAdded = true;
2938
        }
2939
    }
2940
2941
    /**
2942
     * removes any spaces from the 'toAdd' bit and adds the pipe if there is
2943
     * anything to add at all.  Through the lang files, you can change the pipe
2944
     * symbol to anything you like.
2945
     *
2946
     * @param  string $pipe
2947
     * @param  string $toAdd
2948
     * @return string
2949
     */
2950
    protected function cleanSecondaryTitleForAddition($pipe, $toAdd)
2951
    {
2952
        $toAdd = trim($toAdd);
2953
        $length = strlen($toAdd);
2954
        if ($length > 0) {
2955
            $toAdd = $pipe.$toAdd;
2956
        }
2957
        return $toAdd;
2958
    }
2959
2960
    /****************************************************
2961
     *  DEBUG
2962
    /****************************************************/
2963
2964
    public function debug()
2965
    {
2966
        $member = Member::currentUser();
2967
        if (!$member || !$member->IsShopAdmin()) {
2968
            $messages = array(
2969
                'default' => 'You must login as an admin to use debug functions.',
2970
            );
2971
            Security::permissionFailure($this, $messages);
2972
        }
2973
        $this->ProductsShowable();
2974
        $html = EcommerceTaskDebugCart::debug_object($this->dataRecord);
2975
        $html .= '<ul>';
2976
2977
        $html .= '<li><hr /><h3>Available options</h3><hr /></li>';
2978
        $html .= '<li><b>Sort Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('SORT'), 1).'</pre> </li>';
2979
        $html .= '<li><b>Filter Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('FILTER'), 1).'</pre></li>';
2980
        $html .= '<li><b>Display Styles for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('DISPLAY'), 1).'</pre> </li>';
2981
2982
        $html .= '<li><hr /><h3>Selection Setting (what is set as default for this page)</h3><hr /></li>';
2983
        $html .= '<li><b>MyDefaultFilter:</b> '.$this->getMyUserPreferencesDefault('FILTER').' </li>';
2984
        $html .= '<li><b>MyDefaultSortOrder:</b> '.$this->getMyUserPreferencesDefault('SORT').' </li>';
2985
        $html .= '<li><b>MyDefaultDisplayStyle:</b> '.$this->getMyUserPreferencesDefault('DISPLAY').' </li>';
2986
        $html .= '<li><b>MyNumberOfProductsPerPage:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
2987
        $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>';
2988
2989
        $html .= '<li><hr /><h3>Current Settings</h3><hr /></li>';
2990
        $html .= '<li><b>Current Sort Order:</b> '.$this->getCurrentUserPreferences('SORT').' </li>';
2991
        $html .= '<li><b>Current Filter:</b> '.$this->getCurrentUserPreferences('FILTER').' </li>';
2992
        $html .= '<li><b>Current display style:</b> '.$this->getCurrentUserPreferences('DISPLAY').' </li>';
2993
2994
        $html .= '<li><hr /><h3>DATALIST: totals, numbers per page etc</h3><hr /></li>';
2995
        $html .= '<li><b>Total number of products:</b> '.$this->TotalCount().' </li>';
2996
        $html .= '<li><b>Is there more than one product:</b> '.($this->TotalCountGreaterThanOne() ? 'YES' : 'NO').' </li>';
2997
        $html .= '<li><b>Number of products per page:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
2998
2999
        $html .= '<li><hr /><h3>SQL Factors</h3><hr /></li>';
3000
        $html .= '<li><b>Default sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT'), 1).' </li>';
3001
        $html .= '<li><b>User sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT', $this->getCurrentUserPreferences('SORT')), 1).' </li>';
3002
        $html .= '<li><b>Default Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER'), 1).'</pre> </li>';
3003
        $html .= '<li><b>User Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER', $this->getCurrentUserPreferences('FILTER')), 1).'</pre> </li>';
3004
        $html .= '<li><b>Buyable Class name:</b> '.$this->getBuyableClassName().' </li>';
3005
        $html .= '<li><b>allProducts:</b> '.print_r(str_replace('"', '`', $this->allProducts->sql()), 1).' </li>';
3006
3007
        $html .= '<li><hr /><h3>Search</h3><hr /></li>';
3008
        $resultArray = $this->searchResultsArrayFromSession();
3009
        $productGroupArray = explode(',', Session::get($this->SearchResultsSessionVariable(true)));
3010
        $html .= '<li><b>Is Search Results:</b> '.($this->IsSearchResults() ? 'YES' : 'NO').' </li>';
3011
        $html .= '<li><b>Products In Search (session variable : '.$this->SearchResultsSessionVariable(false).'):</b> '.print_r($resultArray, 1).' </li>';
3012
        $html .= '<li><b>Product Groups In Search (session variable : '.$this->SearchResultsSessionVariable(true).'):</b> '.print_r($productGroupArray, 1).' </li>';
3013
3014
        $html .= '<li><hr /><h3>Other</h3><hr /></li>';
3015
        if ($image = $this->BestAvailableImage()) {
3016
            $html .= '<li><b>Best Available Image:</b> <img src="'.$image->Link.'" /> </li>';
3017
        }
3018
        $html .= '<li><b>BestAvailableImage:</b> '.($this->BestAvailableImage() ? $this->BestAvailableImage()->Link : 'no image available').' </li>';
3019
        $html .= '<li><b>Is this an ecommerce page:</b> '.($this->IsEcommercePage() ? 'YES' : 'NO').' </li>';
3020
        $html .= '<li><hr /><h3>Related Groups</h3><hr /></li>';
3021
        $html .= '<li><b>Parent product group:</b> '.($this->ParentGroup() ? $this->ParentGroup()->Title : '[NO PARENT GROUP]').'</li>';
3022
3023
        $childGroups = $this->ChildGroups(99);
3024
        if ($childGroups->count()) {
3025
            $childGroups = $childGroups->map('ID', 'MenuTitle');
3026
            $html .= '<li><b>Child Groups (all):</b><pre> '.print_r($childGroups, 1).' </pre></li>';
3027
        } else {
3028
            $html .= '<li><b>Child Groups (full tree): </b>NONE</li>';
3029
        }
3030
        $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>';
3031
        $html .= '<li><b>the inverse of ProductGroupsFromAlsoShowProducts:</b><pre> '.print_r($this->ProductGroupsFromAlsoShowProductsInverse()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
3032
        $html .= '<li><b>all product parent groups:</b><pre> '.print_r($this->ProductGroupsParentGroups()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
3033
3034
        $html .= '<li><hr /><h3>Product Example and Links</h3><hr /></li>';
3035
        $product = DataObject::get_one(
3036
            'Product',
3037
            array('ParentID' => $this->ID)
3038
        );
3039
        if ($product) {
3040
            $html .= '<li><b>Product View:</b> <a href="'.$product->Link().'">'.$product->Title.'</a> </li>';
3041
            $html .= '<li><b>Product Debug:</b> <a href="'.$product->Link('debug').'">'.$product->Title.'</a> </li>';
3042
            $html .= '<li><b>Product Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$product->ID.'">'.$product->Title.'</a> </li>';
3043
            $html .= '<li><b>ProductGroup Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$this->ID.'">'.$this->Title.'</a> </li>';
3044
        } else {
3045
            $html .= '<li>this page has no products of its own</li>';
3046
        }
3047
        $html .= '</ul>';
3048
3049
        return $html;
3050
    }
3051
}
3052