Completed
Push — master ( 613b2e...a2195f )
by Nicolaas
03:28
created

ProductGroup_Controller::debug()   F

Complexity

Conditions 12
Paths 1024

Size

Total Lines 84
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
dl 0
loc 84
rs 2
c 0
b 0
f 0
eloc 67
nc 1024
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    public function MyNumberOfProductsPerPage()
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...
584
    {
585
        $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...
586
        if ($this->NumberOfProductsPerPage) {
587
            $productsPagePage = $this->NumberOfProductsPerPage;
588
        } else {
589
            if ($parent = $this->ParentGroup()) {
590
                $productsPagePage = $parent->MyNumberOfProductsPerPage();
591
            } else {
592
                $productsPagePage = $this->EcomConfig()->NumberOfProductsPerPage;
593
            }
594
        }
595
596
        return $productsPagePage;
597
    }
598
599
    /*********************
600
     * SETTINGS: level of products to show
601
     *********************/
602
603
    /**
604
     * returns the number of product groups (children)
605
     * to show in the current product list
606
     * based on the user setting for this page.
607
     *
608
     * @return int
609
     */
610
    public function MyLevelOfProductsToShow()
611
    {
612
        if ($this->LevelOfProductsToShow == 0) {
613
            if ($parent = $this->ParentGroup()) {
614
                $this->LevelOfProductsToShow = $parent->MyLevelOfProductsToShow();
615
            }
616
        }
617
        //reset to default
618
        if ($this->LevelOfProductsToShow     == 0) {
619
            $defaults = Config::inst()->get('ProductGroup', 'defaults');
620
621
            return isset($defaults['LevelOfProductsToShow']) ? $defaults['LevelOfProductsToShow'] : 99;
622
        }
623
624
        return $this->LevelOfProductsToShow;
625
    }
626
627
    /*********************
628
     * CMS Fields
629
     *********************/
630
631
    /**
632
     * standard SS method.
633
     *
634
     * @return FieldList
635
     */
636
    public function getCMSFields()
637
    {
638
        $fields = parent::getCMSFields();
639
        //dirty hack to show images!
640
        $fields->addFieldToTab('Root.Images', Product_ProductImageUploadField::create('Image', _t('Product.IMAGE', 'Product Group Image')));
641
        //number of products
642
        $calculatedNumberOfProductsPerPage = $this->MyNumberOfProductsPerPage();
643
        $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)') : '';
644
        $fields->addFieldToTab(
645
            'Root',
646
            Tab::create(
647
                'ProductDisplay',
648
                _t('ProductGroup.DISPLAY', 'Display'),
649
                $productsToShowField = DropdownField::create('LevelOfProductsToShow', _t('ProductGroup.PRODUCTSTOSHOW', 'Products to show'), $this->showProductLevels),
650
                HeaderField::create('WhatProductsAreShown', _t('ProductGroup.WHATPRODUCTSSHOWN', _t('ProductGroup.OPTIONSSELECTEDBELOWAPPLYTOCHILDGROUPS', 'Inherited options'))),
651
                $numberOfProductsPerPageField = NumericField::create('NumberOfProductsPerPage', _t('ProductGroup.PRODUCTSPERPAGE', 'Number of products per page'))
652
            )
653
        );
654
        $numberOfProductsPerPageField->setRightTitle($numberOfProductsPerPageExplanation);
655
        if ($calculatedNumberOfProductsPerPage && !$this->NumberOfProductsPerPage) {
656
            $this->NumberOfProductsPerPage = null;
657
            $numberOfProductsPerPageField->setAttribute('placeholder', $calculatedNumberOfProductsPerPage);
658
        }
659
        //sort
660
        $sortDropdownList = $this->getUserPreferencesOptionsForDropdown('SORT');
661
        if (count($sortDropdownList) > 1) {
662
            $sortOrderKey = $this->getMyUserPreferencesDefault('SORT');
663
            if ($this->DefaultSortOrder == 'inherit') {
664
                $actualValue = ' ('.(isset($sortDropdownList[$sortOrderKey]) ? $sortDropdownList[$sortOrderKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
665
                $sortDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
666
            }
667
            $fields->addFieldToTab(
668
                'Root.ProductDisplay',
669
                $defaultSortOrderField = DropdownField::create('DefaultSortOrder', _t('ProductGroup.DEFAULTSORTORDER', 'Default Sort Order'), $sortDropdownList)
670
            );
671
            $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."));
672
        }
673
        //filter
674
        $filterDropdownList = $this->getUserPreferencesOptionsForDropdown('FILTER');
675
        if (count($filterDropdownList) > 1) {
676
            $filterKey = $this->getMyUserPreferencesDefault('FILTER');
677
            if ($this->DefaultFilter == 'inherit') {
678
                $actualValue = ' ('.(isset($filterDropdownList[$filterKey]) ? $filterDropdownList[$filterKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
679
                $filterDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
680
            }
681
            $fields->addFieldToTab(
682
                'Root.ProductDisplay',
683
                $defaultFilterField = DropdownField::create('DefaultFilter', _t('ProductGroup.DEFAULTFILTER', 'Default Filter'), $filterDropdownList)
684
            );
685
            $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."));
686
        }
687
        //display style
688
        $displayStyleDropdownList = $this->getUserPreferencesOptionsForDropdown('DISPLAY');
689
        if (count($displayStyleDropdownList) > 2) {
690
            $displayStyleKey = $this->getMyUserPreferencesDefault('DISPLAY');
691
            if ($this->DisplayStyle == 'inherit') {
692
                $actualValue = ' ('.(isset($displayStyleDropdownList[$displayStyleKey]) ? $displayStyleDropdownList[$displayStyleKey] : _t('ProductGroup.ERROR', 'ERROR')).')';
693
                $displayStyleDropdownList['inherit'] = _t('ProductGroup.INHERIT', 'Inherit').$actualValue;
694
            }
695
            $fields->addFieldToTab(
696
                'Root.ProductDisplay',
697
                DropdownField::create('DisplayStyle', _t('ProductGroup.DEFAULTDISPLAYSTYLE', 'Default Display Style'), $displayStyleDropdownList)
698
            );
699
        }
700
        if ($this->EcomConfig()->ProductsAlsoInOtherGroups) {
701
            if (!$this instanceof ProductGroupSearchPage) {
702
                $fields->addFieldsToTab(
703
                    'Root.OtherProductsShown',
704
                    array(
705
                        HeaderField::create('ProductGroupsHeader', _t('ProductGroup.OTHERPRODUCTSTOSHOW', 'Other products to show ...')),
706
                        $this->getProductGroupsTable(),
707
                    )
708
                );
709
            }
710
        }
711
712
        return $fields;
713
    }
714
715
    /**
716
     * used if you install lumberjack
717
     * @return string
718
     */
719
    public function getLumberjackTitle()
720
    {
721
        return _t('ProductGroup.BUYABLES', 'Products');
722
    }
723
724
    // /**
725
    //  * used if you install lumberjack
726
    //  * @return string
727
    //  */
728
    // 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...
729
    // {
730
    //     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...
731
    // }
732
733
    /**
734
     * Used in getCSMFields.
735
     *
736
     * @return GridField
737
     **/
738
    protected function getProductGroupsTable()
739
    {
740
        $gridField = GridField::create(
741
            'AlsoShowProducts',
742
            _t('ProductGroup.OTHER_PRODUCTS_SHOWN_IN_THIS_GROUP', 'Other products shown in this group ...'),
743
            $this->AlsoShowProducts(),
744
            GridFieldBasicPageRelationConfig::create()
745
        );
746
        //make sure edits are done in the right place ...
747
        return $gridField;
748
    }
749
750
    /*****************************************************
751
     *
752
     *
753
     *
754
     * PRODUCTS THAT BELONG WITH THIS PRODUCT GROUP
755
     *
756
     *
757
     *
758
     *****************************************************/
759
760
    /**
761
     * returns the inital (all) products, based on the all the eligible products
762
     * for the page.
763
     *
764
     * This is THE pivotal method that probably changes for classes that
765
     * extend ProductGroup as here you can determine what products or other buyables are shown.
766
     *
767
     * The return from this method will then be sorted to produce the final product list.
768
     *
769
     * There is no sort for the initial retrieval
770
     *
771
     * This method is public so that you can retrieve a list of products for a product group page.
772
     *
773
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
774
     * @param string         $alternativeFilterKey Alternative standard filter to be used.
775
     *
776
     * @return DataList
777
     **/
778
    public function currentInitialProducts($extraFilter = null, $alternativeFilterKey = '')
779
    {
780
781
        //INIT ALLPRODUCTS
782
        unset($this->allProducts);
783
        $className = $this->getBuyableClassName();
784
        $this->allProducts = $className::get();
785
786
        // GROUP FILTER (PRODUCTS FOR THIS GROUP)
787
        $this->allProducts = $this->getGroupFilter();
788
789
        // STANDARD FILTER (INCLUDES USER PREFERENCE)
790
        $filterStatement = $this->allowPurchaseWhereStatement();
791
        if ($filterStatement) {
792
            if (is_array($filterStatement)) {
793
                $this->allProducts = $this->allProducts->filter($filterStatement);
794
            } elseif (is_string($filterStatement)) {
795
                $this->allProducts = $this->allProducts->where($filterStatement);
796
            }
797
        }
798
        $this->allProducts = $this->getStandardFilter($alternativeFilterKey);
799
800
        // EXTRA FILTER (ON THE FLY FROM CONTROLLER)
801
        if (is_array($extraFilter) && count($extraFilter)) {
802
            $this->allProducts = $this->allProducts->filter($extraFilter);
803
        } elseif (is_string($extraFilter) && strlen($extraFilter) > 2) {
804
            $this->allProducts = $this->allProducts->where($extraFilter);
805
        }
806
807
        //JOINS
808
        $this->allProducts = $this->getGroupJoin();
809
810
        return $this->allProducts;
811
    }
812
813
    /**
814
     * this method can be used quickly current initial products
815
     * whenever you write:
816
     *  ```php
817
     *   currentInitialProducts->(null, $key)->map("ID", "ID")->toArray();
818
     *  ```
819
     * this is the better replacement.
820
     *
821
     * @param string $filterKey
822
     *
823
     * @return array
824
     */
825
    public function currentInitialProductsAsCachedArray($filterKey)
826
    {
827
        $cacheKey = 'CurrentInitialProductsArray'.$filterKey;
828
        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...
829
            //do nothing
830
        } else {
831
            $array = $this->currentInitialProducts(null, $filterKey)->map('ID', 'ID')->toArray();
832
            $this->saveObjectStore($array, $cacheKey);
833
        }
834
835
        return $array;
836
    }
837
838
    /*****************************************************
839
     * DATALIST: adjusters
840
     * these are the methods you want to override in
841
     * any clases that extend ProductGroup
842
     *****************************************************/
843
844
    /**
845
     * Do products occur in more than one group.
846
     *
847
     * @return bool
848
     */
849
    protected function getProductsAlsoInOtherGroups()
850
    {
851
        return $this->EcomConfig()->ProductsAlsoInOtherGroups;
852
    }
853
854
    /**
855
     * Returns the class we are working with.
856
     *
857
     * @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...
858
     */
859
    protected function getBuyableClassName()
860
    {
861
        return EcommerceConfig::get('ProductGroup', 'base_buyable_class');
862
    }
863
864
    /**
865
     * @SEE: important notes at the top of this file / class
866
     *
867
     * IMPORTANT: Adjusts allProducts and returns it...
868
     *
869
     * @return DataList
870
     */
871
    protected function getGroupFilter()
872
    {
873
        $levelToShow = $this->MyLevelOfProductsToShow();
874
        $cacheKey = 'GroupFilter_'.abs(intval($levelToShow + 999));
875
        if ($groupFilter = $this->retrieveObjectStore($cacheKey)) {
876
            $this->allProducts = $this->allProducts->where($groupFilter);
877
        } else {
878
            $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...
879
            $productFilterArray = array();
880
            //special cases
881
            if ($levelToShow < 0) {
882
                //no produts but if LevelOfProductsToShow = -1 then show all
883
                $groupFilter = ' ('.$levelToShow.' = -1) ';
884
            } elseif ($levelToShow > 0) {
885
                $groupIDs = array($this->ID => $this->ID);
886
                $productFilterTemp = $this->getProductsToBeIncludedFromOtherGroups();
887
                $productFilterArray[$productFilterTemp] = $productFilterTemp;
888
                $childGroups = $this->ChildGroups($levelToShow);
889
                if ($childGroups && $childGroups->count()) {
890
                    foreach ($childGroups as $childGroup) {
891
                        $groupIDs[$childGroup->ID] = $childGroup->ID;
892
                        $productFilterTemp = $childGroup->getProductsToBeIncludedFromOtherGroups();
893
                        $productFilterArray[$productFilterTemp] = $productFilterTemp;
894
                    }
895
                }
896
                $groupFilter = ' ( "ParentID" IN ('.implode(',', $groupIDs).') ) '.implode($productFilterArray).' ';
897
            } else {
898
                //fall-back
899
                $groupFilter = '"ParentID" < 0';
900
            }
901
            $this->allProducts = $this->allProducts->where($groupFilter);
902
            $this->saveObjectStore($groupFilter, $cacheKey);
903
        }
904
905
        return $this->allProducts;
906
    }
907
908
    /**
909
     * If products are show in more than one group
910
     * Then this returns a where phrase for any products that are linked to this
911
     * product group.
912
     *
913
     * @return string
914
     */
915
    protected function getProductsToBeIncludedFromOtherGroups()
916
    {
917
        //TO DO: this should actually return
918
        //Product.ID = IN ARRAY(bla bla)
919
        $array = array();
920
        if ($this->getProductsAlsoInOtherGroups()) {
921
            $array = $this->AlsoShowProducts()->map('ID', 'ID')->toArray();
922
        }
923
        if (count($array)) {
924
            $stage = $this->getStage();
925
926
            return " OR (\"Product$stage\".\"ID\" IN (".implode(',', $array).')) ';
927
        }
928
929
        return '';
930
    }
931
932
    /**
933
     * @SEE: important notes at the top of this class / file for more information!
934
     *
935
     * IMPORTANT: Adjusts allProducts and returns it...
936
     *
937
     * @param string $alternativeFilterKey - filter key to be used... if none is specified then we use the current one.
938
     *
939
     * @return DataList
940
     */
941
    protected function getStandardFilter($alternativeFilterKey = '')
942
    {
943
        if ($alternativeFilterKey) {
944
            $filterKey = $alternativeFilterKey;
945
        } else {
946
            $filterKey = $this->getCurrentUserPreferences('FILTER');
947
        }
948
        $filter = $this->getUserSettingsOptionSQL('FILTER', $filterKey);
949
        if (is_array($filter)) {
950
            $this->allProducts = $this->allProducts->Filter($filter);
951
        } elseif (is_string($filter) && strlen($filter) > 2) {
952
            $this->allProducts = $this->allProducts->Where($filter);
953
        }
954
955
        return $this->allProducts;
956
    }
957
958
    /**
959
     * Join statement for the product groups.
960
     *
961
     * IMPORTANT: Adjusts allProducts and returns it...
962
     *
963
     * @return DataList
964
     */
965
    protected function getGroupJoin()
966
    {
967
        return $this->allProducts;
968
    }
969
970
    /**
971
     * Quick - dirty hack - filter to
972
     * only show relevant products.
973
     *
974
     * @param bool   $asArray
975
     * @param string $table
976
     */
977
    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...
978
    {
979
        if ($this->EcomConfig()->OnlyShowProductsThatCanBePurchased) {
980
            if ($asArray) {
981
                $allowPurchaseWhereStatement = array('AllowPurchase' => 1);
982
            } else {
983
                $allowPurchaseWhereStatement = "\"$table\".\"AllowPurchase\" = 1  ";
984
            }
985
986
            return $allowPurchaseWhereStatement;
987
        }
988
    }
989
990
    /*****************************************************
991
     *
992
     *
993
     *
994
     *
995
     * FINAL PRODUCTS
996
     *
997
     *
998
     *
999
     *
1000
     *****************************************************/
1001
1002
    /**
1003
     * This is the dataList that contains all the products.
1004
     *
1005
     * @var DataList
1006
     */
1007
    protected $allProducts = null;
1008
1009
    /**
1010
     * a list of relevant buyables that can
1011
     * not be purchased and therefore should be excluded.
1012
     * Should be set to NULL to start with so we know if it has been
1013
     * set yet.
1014
     *
1015
     * @var null | Array (like so: array(1,2,4,5,99))
1016
     */
1017
    private $canNOTbePurchasedArray = null;
1018
1019
    /**
1020
     * a list of relevant buyables that can
1021
     * be purchased.  We keep this so that
1022
     * that we can save to session, etc... for future use.
1023
     * Should be set to NULL to start with so we know if it has been
1024
     * set yet.
1025
     *
1026
     * @var null | Array (like so: array(1,2,4,5,99))
1027
     */
1028
    protected $canBePurchasedArray = null;
1029
1030
    /**
1031
     * returns the total numer of products (before pagination).
1032
     *
1033
     * @return int
1034
     **/
1035
    public function RawCount()
1036
    {
1037
        return $this->rawCount ? $this->rawCount : 0;
1038
    }
1039
1040
    /**
1041
     * returns the total numer of products (before pagination).
1042
     *
1043
     * @return int
1044
     **/
1045
    public function TotalCount()
1046
    {
1047
        return $this->totalCount ? $this->totalCount : 0;
1048
    }
1049
1050
    /**
1051
     * this is used to save a list of sorted products
1052
     * so that you can find a previous and a next button, etc...
1053
     *
1054
     * @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...
1055
     */
1056
    public function getProductsThatCanBePurchasedArray()
1057
    {
1058
        return $this->canBePurchasedArray;
1059
    }
1060
1061
    /**
1062
     * Retrieve a set of products, based on the given parameters.
1063
     * This method is usually called by the various controller methods.
1064
     * The extraFilter helps you to select different products,
1065
     * depending on the method used in the controller.
1066
     *
1067
     * Furthermore, extrafilter can take all sorts of variables.
1068
     * This is basically setup like this so that in ProductGroup extensions you
1069
     * can setup all sorts of filters, while still using the ProductsShowable method.
1070
     *
1071
     * The extra filter can be supplied as array (e.g. array("ID" => 12) or array("ID" => array(12,13,45)))
1072
     * or as string. Arrays are used like this $productDataList->filter($array) and
1073
     * strings are used with the where commands $productDataList->where($string).
1074
     *
1075
     * @param array | string $extraFilter          Additional SQL filters to apply to the Product retrieval
1076
     * @param array | string $alternativeSort      Additional SQL for sorting
1077
     * @param string         $alternativeFilterKey alternative filter key to be used
1078
     *
1079
     * @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...
1080
     */
1081
    public function ProductsShowable($extraFilter = null, $alternativeSort = null, $alternativeFilterKey = '')
1082
    {
1083
1084
        //get original products without sort
1085
        $this->allProducts = $this->currentInitialProducts($extraFilter, $alternativeFilterKey);
1086
1087
        //sort products
1088
        $this->allProducts = $this->currentFinalProducts($alternativeSort);
1089
1090
        return $this->allProducts;
1091
    }
1092
1093
    /**
1094
     * returns the final products, based on the all the eligile products
1095
     * for the page.
1096
     *
1097
     * In the process we also save a list of included products
1098
     * and we sort them.  We also keep a record of the total count.
1099
     *
1100
     * All of the 'current' methods are to support the currentFinalProducts Method.
1101
     *
1102
     * @TODO: cache data for faster access.
1103
     *
1104
     * @param array | string $alternativeSort = Alternative Sort String or array
1105
     *
1106
     * @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...
1107
     **/
1108
    protected function currentFinalProducts($alternativeSort = null)
1109
    {
1110
        if ($this->allProducts) {
1111
1112
            //limit to maximum number of products for speed's sake
1113
            $this->allProducts = $this->sortCurrentFinalProducts($alternativeSort);
1114
            $this->allProducts = $this->limitCurrentFinalProducts();
1115
            $this->allProducts = $this->removeExcludedProductsAndSaveIncludedProducts($this->allProducts);
1116
1117
            return $this->allProducts;
1118
        }
1119
    }
1120
1121
    /**
1122
     * returns the SORT part of the final selection of products.
1123
     *
1124
     * @return DataList (allProducts)
1125
     */
1126
    protected function sortCurrentFinalProducts($alternativeSort)
1127
    {
1128
        if ($alternativeSort) {
1129
            if ($this->IsIDarray($alternativeSort)) {
1130
                $sort = $this->createSortStatementFromIDArray($alternativeSort);
1131
            } else {
1132
                $sort = $alternativeSort;
1133
            }
1134
        } else {
1135
            $sort = $this->currentSortSQL();
1136
        }
1137
        $this->allProducts = $this->allProducts->Sort($sort);
1138
1139
        return $this->allProducts;
1140
    }
1141
1142
    /**
1143
     * is the variable provided is an array
1144
     * that can be used as a list of IDs?
1145
     *
1146
     * @param mixed
1147
     *
1148
     * @return bool
1149
     */
1150
    protected function IsIDarray($variable)
1151
    {
1152
        return $variable && is_array($variable) && count($variable) && intval(current($variable)) == current($variable);
1153
    }
1154
1155
    /**
1156
     * returns the SORT part of the final selection of products.
1157
     *
1158
     * @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...
1159
     */
1160
    protected function currentSortSQL()
1161
    {
1162
        $sortKey = $this->getCurrentUserPreferences('SORT');
1163
1164
        return $this->getUserSettingsOptionSQL('SORT', $sortKey);
1165
    }
1166
1167
    /**
1168
     * creates a sort string from a list of ID arrays...
1169
     *
1170
     * @param array $IDarray - list of product IDs
1171
     *
1172
     * @return string
1173
     */
1174
    protected function createSortStatementFromIDArray($IDarray, $table = 'Product')
1175
    {
1176
        $ifStatement = 'CASE ';
1177
        $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...
1178
        $stage = $this->getStage();
1179
        $count = 0;
1180
        foreach ($IDarray as $productID) {
1181
            $ifStatement .= ' WHEN "'.$table.$stage."\".\"ID\" = $productID THEN $count";
1182
            ++$count;
1183
        }
1184
        $sortStatement = $ifStatement.' END';
1185
1186
        return $sortStatement;
1187
    }
1188
1189
    /**
1190
     * limits the products to a maximum number (for speed's sake).
1191
     *
1192
     * @return DataList (this->allProducts adjusted!)
1193
     */
1194
    protected function limitCurrentFinalProducts()
1195
    {
1196
        $this->rawCount = $this->allProducts->count();
1197
        $max = EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list');
1198
        if ($this->rawCount > $max) {
1199
            $this->allProducts = $this->allProducts->limit($max);
1200
            $this->totalCount = $max;
1201
        } else {
1202
            $this->totalCount = $this->rawCount;
1203
        }
1204
1205
        return $this->allProducts;
1206
    }
1207
1208
    /**
1209
     * Excluded products that can not be purchased
1210
     * We all make a record of all the products that are in the current list
1211
     * For efficiency sake, we do both these things at the same time.
1212
     * IMPORTANT: Adjusts allProducts and returns it...
1213
     *
1214
     * @todo: cache data per user ....
1215
     *
1216
     * @return DataList
1217
     */
1218
    protected function removeExcludedProductsAndSaveIncludedProducts()
1219
    {
1220
        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...
1221
            //already done!
1222
        } else {
1223
            $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...
1224
            $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...
1225
            if ($this->config()->get('actively_check_for_can_purchase')) {
1226
                foreach ($this->allProducts as $buyable) {
1227
                    if ($buyable->canPurchase()) {
1228
                        $this->canBePurchasedArray[$buyable->ID] = $buyable->ID;
1229
                    } else {
1230
                        $this->canNOTbePurchasedArray[$buyable->ID] = $buyable->ID;
1231
                    }
1232
                }
1233
            } else {
1234
                if ($this->rawCount > 0) {
1235
                    $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...
1236
                } else {
1237
                    $this->canBePurchasedArray = array();
1238
                }
1239
            }
1240
            if (count($this->canNOTbePurchasedArray)) {
1241
                $this->allProducts = $this->allProducts->Exclude(array('ID' => $this->canNOTbePurchasedArray));
1242
            }
1243
        }
1244
1245
        return $this->allProducts;
1246
    }
1247
1248
    /*****************************************************
1249
     * Children and Parents
1250
     *****************************************************/
1251
1252
    /**
1253
     * Returns children ProductGroup pages of this group.
1254
     *
1255
     * @param int            $maxRecursiveLevel  - maximum depth , e.g. 1 = one level down - so no Child Groups are returned...
1256
     * @param string | Array $filter             - additional filter to be added
1257
     * @param int            $numberOfRecursions - current level of depth
1258
     *
1259
     * @return ArrayList (ProductGroups)
1260
     */
1261
    public function ChildGroups($maxRecursiveLevel, $filter = null, $numberOfRecursions = 0)
1262
    {
1263
        $arrayList = ArrayList::create();
1264
        ++$numberOfRecursions;
1265
        if ($numberOfRecursions < $maxRecursiveLevel) {
1266
            if ($filter && is_string($filter)) {
1267
                $filterWithAND = " AND $filter";
1268
                $where = "\"ParentID\" = '$this->ID' $filterWithAND";
1269
                $children = ProductGroup::get()->where($where);
1270
            } elseif (is_array($filter) && count($filter)) {
1271
                $filter = $filter + array('ParentID' => $this->ID);
1272
                $children = ProductGroup::get()->filter($filter);
1273
            } else {
1274
                $children = ProductGroup::get()->filter(array('ParentID' => $this->ID));
1275
            }
1276
1277
            if ($children->count()) {
1278
                foreach ($children as $child) {
1279
                    $arrayList->push($child);
1280
                    $arrayList->merge($child->ChildGroups($maxRecursiveLevel, $filter, $numberOfRecursions));
1281
                }
1282
            }
1283
        }
1284
        if (!$arrayList instanceof ArrayList) {
1285
            user_error('We expect an array list as output');
1286
        }
1287
1288
        return $arrayList;
1289
    }
1290
1291
    /**
1292
     * Deprecated method.
1293
     */
1294
    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...
1295
    {
1296
        Deprecation::notice('3.1', 'No longer in use');
1297
        if ($maxRecursiveLevel > 24) {
1298
            $maxRecursiveLevel = 24;
1299
        }
1300
1301
        $stage = $this->getStage();
1302
        $select = 'P1.ID as ID1 ';
1303
        $from = "ProductGroup$stage as P1 ";
1304
        $join = " INNER JOIN SiteTree$stage AS S1 ON P1.ID = S1.ID";
1305
        $where = '1 = 1';
1306
        $ids = array(-1);
1307
        for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1308
            $j = $i + 1;
1309
            $select .= ", P$j.ID AS ID$j, S$j.ParentID";
1310
            $join .= "
1311
                LEFT JOIN ProductGroup$stage AS P$j ON P$j.ID = S$i.ParentID
1312
                LEFT JOIN SiteTree$stage AS S$j ON P$j.ID = S$j.ID
1313
            ";
1314
        }
1315
        $rows = DB::Query(' SELECT '.$select.' FROM '.$from.$join.' WHERE '.$where);
1316
        if ($rows) {
1317
            foreach ($rows as $row) {
1318
                for ($i = 1; $i < $maxRecursiveLevel; ++$i) {
1319
                    if ($row['ID'.$i]) {
1320
                        $ids[$row['ID'.$i]] = $row['ID'.$i];
1321
                    }
1322
                }
1323
            }
1324
        }
1325
1326
        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...
1327
    }
1328
1329
    /**
1330
     * returns the parent page, but only if it is an instance of Product Group.
1331
     *
1332
     * @return DataObject | Null (ProductGroup)
1333
     **/
1334
    public function ParentGroup()
1335
    {
1336
        if ($this->ParentID) {
1337
            return ProductGroup::get()->byID($this->ParentID);
1338
        }
1339
    }
1340
1341
    /*****************************************************
1342
     * Other Stuff
1343
     *****************************************************/
1344
1345
    /**
1346
     * Recursively generate a product menu.
1347
     *
1348
     * @param string $filter
1349
     *
1350
     * @return ArrayList (ProductGroups)
1351
     */
1352
    public function GroupsMenu($filter = 'ShowInMenus = 1')
1353
    {
1354
        if ($parent = $this->ParentGroup()) {
1355
            return is_a($parent, Object::getCustomClass('ProductGroup')) ? $parent->GroupsMenu() : $this->ChildGroups($filter);
1356
        } else {
1357
            return $this->ChildGroups($filter);
1358
        }
1359
    }
1360
1361
    /**
1362
     * returns a "BestAvailable" image if the current one is not available
1363
     * In some cases this is appropriate and in some cases this is not.
1364
     * For example, consider the following setup
1365
     * - product A with three variations
1366
     * - Product A has an image, but the variations have no images
1367
     * With this scenario, you want to show ONLY the product image
1368
     * on the product page, but if one of the variations is added to the
1369
     * cart, then you want to show the product image.
1370
     * This can be achieved bu using the BestAvailable image.
1371
     *
1372
     * @return Image | Null
1373
     */
1374
    public function BestAvailableImage()
1375
    {
1376
        $image = $this->Image();
1377
        if ($image && $image->exists() && file_exists($image->getFullPath())) {
1378
            return $image;
1379
        } elseif ($parent = $this->ParentGroup()) {
1380
            return $parent->BestAvailableImage();
1381
        }
1382
    }
1383
1384
    /*****************************************************
1385
     * Other related products
1386
     *****************************************************/
1387
1388
    /**
1389
     * returns a list of Product Groups that have the products for
1390
     * the CURRENT product group listed as part of their AlsoShowProducts list.
1391
     *
1392
     * EXAMPLE:
1393
     * You can use the AlsoShowProducts to list products by Brand.
1394
     * In general, they are listed under type product groups (e.g. socks, sweaters, t-shirts),
1395
     * and you create a list of separate ProductGroups (brands) that do not have ANY products as children,
1396
     * but link to products using the AlsoShowProducts many_many relation.
1397
     *
1398
     * With the method below you can work out a list of brands that apply to the
1399
     * current product group (e.g. socks come in three brands - namely A, B and C)
1400
     *
1401
     * @return DataList
1402
     */
1403
    public function ProductGroupsFromAlsoShowProducts()
1404
    {
1405
        $parentIDs = array();
1406
        //we need to add the last array to make sure we have some products...
1407
        $myProductsArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1408
        $rows = array();
1409
        if (count($myProductsArray)) {
1410
            $rows = DB::query('
1411
                SELECT "ProductGroupID"
1412
                FROM "Product_ProductGroups"
1413
                WHERE "ProductID" IN ('.implode(',', $myProductsArray).')
1414
                GROUP BY "ProductGroupID";
1415
            ');
1416
        }
1417
        foreach ($rows as $row) {
1418
            $parentIDs[$row['ProductGroupID']] = $row['ProductGroupID'];
1419
        }
1420
        //just in case
1421
        unset($parentIDs[$this->ID]);
1422
        if (!count($parentIDs)) {
1423
            $parentIDs = array(0 => 0);
1424
        }
1425
1426
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1427
    }
1428
1429
    /**
1430
     * This is the inverse of ProductGroupsFromAlsoShowProducts
1431
     * That is, it list the product groups that a product is primarily listed under (exact parents only)
1432
     * from a "AlsoShow" product List.
1433
     *
1434
     * @return DataList
1435
     */
1436
    public function ProductGroupsFromAlsoShowProductsInverse()
1437
    {
1438
        $alsoShowProductsArray = $this->AlsoShowProducts()
1439
            ->filter($this->getUserSettingsOptionSQL('FILTER', $this->getMyUserPreferencesDefault('FILTER')))
1440
            ->map('ID', 'ID')->toArray();
1441
        $alsoShowProductsArray[0] = 0;
1442
        $parentIDs = Product::get()->filter(array('ID' => $alsoShowProductsArray))->map('ParentID', 'ParentID')->toArray();
1443
        //just in case
1444
        unset($parentIDs[$this->ID]);
1445
        if (! count($parentIDs)) {
1446
            $parentIDs = array(0 => 0);
1447
        }
1448
1449
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInMenus' => 1));
1450
    }
1451
1452
    /**
1453
     * given the products for this page,
1454
     * retrieve the parent groups excluding the current one.
1455
     *
1456
     * @return DataList
1457
     */
1458
    public function ProductGroupsParentGroups()
1459
    {
1460
        $arrayOfIDs = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER')) + array(0 => 0);
1461
        $parentIDs = Product::get()->filter(array('ID' => $arrayOfIDs))->map('ParentID', 'ParentID')->toArray();
1462
        //just in case
1463
        unset($parentIDs[$this->ID]);
1464
        if (! count($parentIDs)) {
1465
            $parentIDs = array(0 => 0);
1466
        }
1467
1468
        return ProductGroup::get()->filter(array('ID' => $parentIDs, 'ShowInSearch' => 1));
1469
    }
1470
1471
    /**
1472
     * returns stage as "" or "_Live".
1473
     *
1474
     * @return string
1475
     */
1476
    protected function getStage()
1477
    {
1478
        $stage = '';
1479
        if (Versioned::current_stage() == 'Live') {
1480
            $stage = '_Live';
1481
        }
1482
1483
        return $stage;
1484
    }
1485
1486
    /*****************************************************
1487
     * STANDARD SS METHODS
1488
     *****************************************************/
1489
1490
    /**
1491
     * tells us if the current page is part of e-commerce.
1492
     *
1493
     * @return bool
1494
     */
1495
    public function IsEcommercePage()
1496
    {
1497
        return true;
1498
    }
1499
1500
    public function onAfterWrite()
1501
    {
1502
        parent::onAfterWrite();
1503
        if ($this->ImageID) {
1504
            if ($normalImage = Image::get()->exclude(array('ClassName' => 'Product_Image'))->byID($this->ImageID)) {
1505
                $normalImage = $normalImage->newClassInstance('Product_Image');
1506
                $normalImage->write();
1507
            }
1508
        }
1509
    }
1510
1511
    /*****************************************************
1512
     * CACHING
1513
     *****************************************************/
1514
    /**
1515
     *
1516
     * @return bool
1517
     */
1518
    public function AllowCaching()
1519
    {
1520
        return $this->allowCaching;
1521
    }
1522
1523
    /**
1524
     * keeps a cache of the common caching key element
1525
     * @var string
1526
     */
1527
    private static $_product_group_cache_key_cache = null;
1528
1529
    /**
1530
     *
1531
     * @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...
1532
     * @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...
1533
     *
1534
     * @return string
1535
     */
1536
    public function cacheKey($cacheKey)
1537
    {
1538
        $cacheKey = $cacheKey.'_'.$this->ID;
1539
        if (self::$_product_group_cache_key_cache === null) {
1540
            self::$_product_group_cache_key_cache = "_PR_"
1541
                .strtotime(Product::get()->max('LastEdited')). "_"
1542
                .Product::get()->count();
1543
            self::$_product_group_cache_key_cache .= "PG_"
1544
                .strtotime(ProductGroup::get()->max('LastEdited')). "_"
1545
                .ProductGroup::get()->count();
1546
            if (class_exists('ProductVariation')) {
1547
                self::$_product_group_cache_key_cache .= "PV_"
1548
                  .strtotime(ProductVariation::get()->max('LastEdited')). "_"
1549
                  .ProductVariation::get()->count();
1550
            }
1551
        }
1552
        $cacheKey .= self::$_product_group_cache_key_cache;
1553
1554
        return $cacheKey;
1555
    }
1556
1557
    /**
1558
     * @var Zend_Cache_Core
1559
     */
1560
    protected $silverstripeCoreCache = null;
1561
1562
    /**
1563
     * Set the cache object to use when storing / retrieving partial cache blocks.
1564
     *
1565
     * @param Zend_Cache_Core $silverstripeCoreCache
1566
     */
1567
    public function setSilverstripeCoreCache($silverstripeCoreCache)
1568
    {
1569
        $this->silverstripeCoreCache = $silverstripeCoreCache;
1570
    }
1571
1572
    /**
1573
     * Get the cache object to use when storing / retrieving stuff in the Silverstripe Cache
1574
     *
1575
     * @return Zend_Cache_Core
1576
     */
1577
    protected function getSilverstripeCoreCache()
1578
    {
1579
        return $this->silverstripeCoreCache ? $this->silverstripeCoreCache : SS_Cache::factory('EcomPG');
1580
    }
1581
1582
    /**
1583
     * saving an object to the.
1584
     *
1585
     * @param string $cacheKey
1586
     *
1587
     * @return mixed
1588
     */
1589
    protected function retrieveObjectStore($cacheKey)
1590
    {
1591
        $cacheKey = $this->cacheKey($cacheKey);
1592
        if ($this->AllowCaching()) {
1593
            $cache = $this->getSilverstripeCoreCache();
1594
            $data = $cache->load($cacheKey);
1595
            if (!$data) {
1596
                return;
1597
            }
1598
            if (! $cache->getOption('automatic_serialization')) {
1599
                $data = @unserialize($data);
1600
            }
1601
            return $data;
1602
        }
1603
1604
        return;
1605
    }
1606
1607
    /**
1608
     * returns true when the data is saved...
1609
     *
1610
     * @param mixed  $data
1611
     * @param string $cacheKey - key under which the data is saved...
1612
     *
1613
     * @return bool
1614
     */
1615
    protected function saveObjectStore($data, $cacheKey)
1616
    {
1617
        $cacheKey = $this->cacheKey($cacheKey);
1618
        if ($this->AllowCaching()) {
1619
            $cache = $this->getSilverstripeCoreCache();
1620
            if (! $cache->getOption('automatic_serialization')) {
1621
                $data = serialize($data);
1622
            }
1623
            $cache->save($data, $cacheKey);
1624
            return true;
1625
        }
1626
1627
        return false;
1628
    }
1629
1630
    public function SearchResultsSessionVariable($isForGroups = false)
1631
    {
1632
        $idString = '_'.$this->ID;
1633
        if ($isForGroups) {
1634
            return Config::inst()->get('ProductSearchForm', 'product_session_variable').$idString;
1635
        } else {
1636
            return Config::inst()->get('ProductSearchForm', 'product_group_session_variable').$idString;
1637
        }
1638
    }
1639
1640
    /**
1641
     * cache for result array.
1642
     *
1643
     * @var array
1644
     */
1645
    private static $_result_array = array();
1646
1647
    /**
1648
     * @return array
1649
     */
1650
    public function searchResultsArrayFromSession()
1651
    {
1652
        if (! isset(self::$_result_array[$this->ID]) || self::$_result_array[$this->ID] === null) {
1653
            self::$_result_array[$this->ID] = explode(',', Session::get($this->SearchResultsSessionVariable(false)));
1654
        }
1655
        if (! is_array(self::$_result_array[$this->ID]) || ! count(self::$_result_array[$this->ID])) {
1656
            self::$_result_array[$this->ID] = array(0 => 0);
1657
        }
1658
1659
        return self::$_result_array[$this->ID];
1660
    }
1661
1662
    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...
1663
    {
1664
        return Product::get()->filter(array('ParentID' => $this->ID))->count();
1665
    }
1666
}
1667
1668
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...
1669
{
1670
    /**
1671
     * standard SS variable.
1672
     *
1673
     * @var array
1674
     */
1675
    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...
1676
        'debug' => 'ADMIN',
1677
        'filterforgroup' => true,
1678
        'ProductSearchForm' => true,
1679
        'searchresults' => true,
1680
        'resetfilter' => true,
1681
    );
1682
1683
    /**
1684
     * The original Title of this page before filters, etc...
1685
     *
1686
     * @var string
1687
     */
1688
    protected $originalTitle = '';
1689
1690
    /**
1691
     * list of products that are going to be shown.
1692
     *
1693
     * @var DataList
1694
     */
1695
    protected $products = null;
1696
1697
    /**
1698
     * Show all products on one page?
1699
     *
1700
     * @var bool
1701
     */
1702
    protected $showFullList = false;
1703
1704
    /**
1705
     * The group filter that is applied to this page.
1706
     *
1707
     * @var ProductGroup
1708
     */
1709
    protected $filterForGroupObject = null;
1710
1711
    /**
1712
     * Is this a product search?
1713
     *
1714
     * @var bool
1715
     */
1716
    protected $isSearchResults = false;
1717
1718
    /**
1719
     * standard SS method.
1720
     */
1721
    public function init()
1722
    {
1723
        parent::init();
1724
        $this->originalTitle = $this->Title;
1725
        Requirements::themedCSS('ProductGroup', 'ecommerce');
1726
        Requirements::themedCSS('ProductGroupPopUp', 'ecommerce');
1727
        Requirements::javascript('ecommerce/javascript/EcomProducts.js');
1728
        //we save data from get variables...
1729
        $this->saveUserPreferences();
1730
    }
1731
1732
    /****************************************************
1733
     *  ACTIONS
1734
    /****************************************************/
1735
1736
    /**
1737
     * standard selection of products.
1738
     */
1739
    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...
1740
    {
1741
        //set the filter and the sort...
1742
        $this->addSecondaryTitle();
1743
        $this->products = $this->paginateList($this->ProductsShowable(null));
1744
        if ($this->returnAjaxifiedProductList()) {
1745
            return $this->renderWith('AjaxProductList');
1746
        }
1747
1748
        return array();
1749
    }
1750
1751
    /**
1752
     * cross filter with another product group..
1753
     *
1754
     * e.g. socks (current product group) for brand A or B (the secondary product group)
1755
     *
1756
     * @param HTTPRequest
1757
     */
1758
    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...
1759
    {
1760
        $this->resetfilter();
1761
        $otherGroupURLSegment = Convert::raw2sql($request->param('ID'));
1762
        $arrayOfIDs = array(0 => 0);
1763
        if ($otherGroupURLSegment) {
1764
            $otherProductGroup = ProductGroup::get()->filter(array('URLSegment' => $otherGroupURLSegment))->first();
1765
            if ($otherProductGroup) {
1766
                $this->filterForGroupObject = $otherProductGroup;
1767
                $arrayOfIDs = $otherProductGroup->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
1768
            }
1769
        }
1770
        $this->addSecondaryTitle();
1771
        $this->products = $this->paginateList($this->ProductsShowable(array('ID' => $arrayOfIDs)));
1772
        if ($this->returnAjaxifiedProductList()) {
1773
            return $this->renderWith('AjaxProductList');
1774
        }
1775
1776
        return array();
1777
    }
1778
1779
    /**
1780
     * get the search results.
1781
     *
1782
     * @param HTTPRequest
1783
     */
1784
    public function searchresults($request)
1785
    {
1786
        $this->resetfilter();
1787
        $this->isSearchResults = true;
1788
        //reset filter and sort
1789
        $resultArray = $this->searchResultsArrayFromSession();
1790
        if (!$resultArray || !count($resultArray)) {
1791
            $resultArray = array(0 => 0);
1792
        }
1793
        $defaultKeySort = $this->getMyUserPreferencesDefault('SORT');
1794
        $myKeySort = $this->getCurrentUserPreferences('SORT');
1795
        $searchArray = null;
1796
        if ($defaultKeySort == $myKeySort) {
1797
            $searchArray = $resultArray;
1798
        }
1799
        $this->addSecondaryTitle();
1800
        $this->products = $this->paginateList($this->ProductsShowable(array('ID' => $resultArray), $searchArray));
1801
1802
        return array();
1803
    }
1804
1805
    /**
1806
     * resets the filter only.
1807
     */
1808
    public function resetfilter()
1809
    {
1810
        $defaultKey = $this->getMyUserPreferencesDefault('FILTER');
1811
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
1812
        $this->saveUserPreferences(
1813
            array(
1814
                $filterGetVariable => $defaultKey,
1815
            )
1816
        );
1817
1818
        return array();
1819
    }
1820
1821
    /****************************************************
1822
     *  TEMPLATE METHODS PRODUCTS
1823
    /****************************************************/
1824
1825
    /**
1826
     * Return the products for this group.
1827
     * This is the call that is made from the template...
1828
     * The actual final products being shown.
1829
     *
1830
     * @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...
1831
     **/
1832
    public function Products()
1833
    {
1834
        //IMPORTANT!
1835
        //two universal actions!
1836
        $this->addSecondaryTitle();
1837
        $this->cachingRelatedJavascript();
1838
1839
        //save products to session for later use
1840
        $stringOfIDs = '';
1841
        $array = $this->getProductsThatCanBePurchasedArray();
1842
        if (is_array($array)) {
1843
            $stringOfIDs = implode(',', $array);
1844
        }
1845
        //save list for future use
1846
        Session::set(EcommerceConfig::get('ProductGroup', 'session_name_for_product_array'), $stringOfIDs);
1847
1848
        return $this->products;
1849
    }
1850
1851
    /**
1852
     * you can overload this function of ProductGroup Extensions.
1853
     *
1854
     * @return bool
1855
     */
1856
    protected function returnAjaxifiedProductList()
1857
    {
1858
        return Director::is_ajax() ? true : false;
1859
    }
1860
1861
    /**
1862
     * is the product list cache-able?
1863
     *
1864
     * @return bool
1865
     */
1866
    public function ProductGroupListAreCacheable()
1867
    {
1868
        if ($this->productListsHTMLCanBeCached()) {
1869
            //exception 1
1870
            if ($this->IsSearchResults()) {
1871
                return false;
1872
            }
1873
            //exception 2
1874
            $currentOrder = ShoppingCart::current_order();
1875
            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...
1876
                return false;
1877
            }
1878
            //can be cached...
1879
            return true;
1880
        }
1881
1882
        return false;
1883
    }
1884
1885
    /**
1886
     * is the product list ajaxified.
1887
     *
1888
     * @return bool
1889
     */
1890
    public function ProductGroupListAreAjaxified()
1891
    {
1892
        return $this->IsSearchResults() ? false : true;
1893
    }
1894
1895
    /**
1896
     * Unique caching key for the product list...
1897
     *
1898
     * @return string | Null
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...
1899
     */
1900
    public function ProductGroupListCachingKey()
0 ignored issues
show
Coding Style introduced by
ProductGroupListCachingKey uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1901
    {
1902
        if ($this->ProductGroupListAreCacheable()) {
1903
            $displayKey = $this->getCurrentUserPreferences('DISPLAY');
1904
            $filterKey = $this->getCurrentUserPreferences('FILTER');
1905
            $filterForGroupKey = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
1906
            $sortKey = $this->getCurrentUserPreferences('SORT');
1907
            $pageStart = isset($_GET['start']) ? intval($_GET['start']) : 0;
1908
            $isFullList = $this->IsShowFullList() ? 'Y' : 'N';
1909
1910
            $this->cacheKey(
1911
                implode(
1912
                    '_',
1913
                    array(
1914
                        $displayKey,
1915
                        $filterKey,
1916
                        $filterForGroupKey,
1917
                        $sortKey,
1918
                        $pageStart,
1919
                        $isFullList,
1920
                    )
1921
                )
1922
            );
1923
        }
1924
1925
        return;
1926
    }
1927
1928
    /**
1929
     * adds Javascript to the page to make it work when products are cached.
1930
     */
1931
    public function CachingRelatedJavascript()
1932
    {
1933
        if ($this->ProductGroupListAreAjaxified()) {
1934
            Requirements::customScript("
1935
                    if(typeof EcomCartOptions === 'undefined') {
1936
                        var EcomCartOptions = {};
1937
                    }
1938
                    EcomCartOptions.ajaxifyProductList = true;
1939
                    EcomCartOptions.ajaxifiedListHolderSelector = '#".$this->AjaxDefinitions()->ProductListHolderID()."';
1940
                    EcomCartOptions.ajaxifiedListAdjusterSelectors = '.".$this->AjaxDefinitions()->ProductListAjaxifiedLinkClassName()."';
1941
                    EcomCartOptions.hiddenPageTitleID = '#".$this->AjaxDefinitions()->HiddenPageTitleID()."';
1942
                ",
1943
                'cachingRelatedJavascript_AJAXlist'
1944
            );
1945
        } else {
1946
            Requirements::customScript("
1947
                    if(typeof EcomCartOptions === 'undefined') {
1948
                        var EcomCartOptions = {};
1949
                    }
1950
                    EcomCartOptions.ajaxifyProductList = false;
1951
                ",
1952
                'cachingRelatedJavascript_AJAXlist'
1953
            );
1954
        }
1955
        $currentOrder = ShoppingCart::current_order();
1956
        if ($currentOrder->TotalItems(true)) {
1957
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
1958
            $obj = new $responseClass();
1959
            $obj->setIncludeHeaders(false);
1960
            $json = $obj->ReturnCartData();
1961
            Requirements::customScript("
1962
                    if(typeof EcomCartOptions === 'undefined') {
1963
                        var EcomCartOptions = {};
1964
                    }
1965
                    EcomCartOptions.initialData= ".$json.";
1966
                ",
1967
                'cachingRelatedJavascript_JSON'
1968
            );
1969
        }
1970
    }
1971
1972
    /**
1973
     * you can overload this function of ProductGroup Extensions.
1974
     *
1975
     * @return bool
1976
     */
1977
    protected function productListsHTMLCanBeCached()
1978
    {
1979
        return Config::inst()->get('ProductGroup', 'actively_check_for_can_purchase') ? false : true;
1980
    }
1981
1982
    /*****************************************************
1983
     * DATALIST: totals, number per page, etc..
1984
     *****************************************************/
1985
1986
    /**
1987
     * returns the total numer of products (before pagination).
1988
     *
1989
     * @return bool
1990
     **/
1991
    public function TotalCountGreaterThanOne($greaterThan = 1)
1992
    {
1993
        return $this->TotalCount() > $greaterThan;
1994
    }
1995
1996
    /**
1997
     * have the ProductsShowable been limited.
1998
     *
1999
     * @return bool
2000
     **/
2001
    public function TotalCountGreaterThanMax()
2002
    {
2003
        return $this->RawCount() >  $this->TotalCount();
2004
    }
2005
2006
    /****************************************************
2007
     *  TEMPLATE METHODS MENUS AND SIDEBARS
2008
    /****************************************************/
2009
2010
    /**
2011
     * title without additions.
2012
     *
2013
     * @return string
2014
     */
2015
    public function OriginalTitle()
2016
    {
2017
        return $this->originalTitle;
2018
    }
2019
    /**
2020
     * This method can be extended to show products in the side bar.
2021
     */
2022
    public function SidebarProducts()
2023
    {
2024
        return;
2025
    }
2026
2027
    /**
2028
     * returns child product groups for use in
2029
     * 'in this section'. For example the vegetable Product Group
2030
     * May have listed here: Carrot, Cabbage, etc...
2031
     *
2032
     * @return ArrayList (ProductGroups)
2033
     */
2034
    public function MenuChildGroups()
2035
    {
2036
        return $this->ChildGroups(2, '"ShowInMenus" = 1');
2037
    }
2038
2039
    /**
2040
     * After a search is conducted you may end up with a bunch
2041
     * of recommended product groups. They will be returned here...
2042
     * We sort the list in the order that it is provided.
2043
     *
2044
     * @return DataList | Null (ProductGroups)
2045
     */
2046
    public function SearchResultsChildGroups()
2047
    {
2048
        $groupArray = explode(',', Session::get($this->SearchResultsSessionVariable($isForGroup = true)));
2049
        if (is_array($groupArray) && count($groupArray)) {
2050
            $sortStatement = $this->createSortStatementFromIDArray($groupArray, 'ProductGroup');
2051
2052
            return ProductGroup::get()->filter(array('ID' => $groupArray, 'ShowInSearch' => 1))->sort($sortStatement);
2053
        }
2054
2055
        return;
2056
    }
2057
2058
    /****************************************************
2059
     *  Search Form Related controllers
2060
    /****************************************************/
2061
2062
    /**
2063
     * returns a search form to search current products.
2064
     *
2065
     * @return ProductSearchForm object
2066
     */
2067
    public function ProductSearchForm()
2068
    {
2069
        $onlySearchTitle = $this->originalTitle;
2070
        if ($this->dataRecord instanceof ProductGroupSearchPage) {
2071
            if ($this->HasSearchResults()) {
2072
                $onlySearchTitle = 'Last Search Results';
2073
            }
2074
        }
2075
        $form = ProductSearchForm::create(
2076
            $this,
2077
            'ProductSearchForm',
2078
            $onlySearchTitle,
2079
            $this->currentInitialProducts(null, $this->getMyUserPreferencesDefault('FILTER'))
2080
        );
2081
        $filterGetVariable = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2082
        $sortGetVariable = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2083
        $additionalGetParameters = $filterGetVariable.'='.$this->getMyUserPreferencesDefault('FILTER').'&'.
2084
                                   $sortGetVariable.'='.$this->getMyUserPreferencesDefault('SORT');
2085
        $form->setAdditionalGetParameters($additionalGetParameters);
2086
2087
        return $form;
2088
    }
2089
2090
    /**
2091
     * Does this page have any search results?
2092
     * If search was carried out without returns
2093
     * then it returns zero (false).
2094
     *
2095
     * @return int | false
2096
     */
2097
    public function HasSearchResults()
2098
    {
2099
        $resultArray = $this->searchResultsArrayFromSession();
2100
        if ($resultArray) {
2101
            $count = count($resultArray) - 1;
2102
2103
            return $count ? $count : 0;
2104
        }
2105
2106
        return 0;
2107
    }
2108
2109
    /**
2110
     * Should the product search form be shown immediately?
2111
     *
2112
     * @return bool
2113
     */
2114
    public function ShowSearchFormImmediately()
2115
    {
2116
        if ($this->IsSearchResults()) {
2117
            return true;
2118
        }
2119
        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...
2120
            return false;
2121
        }
2122
2123
        return true;
2124
    }
2125
2126
    /**
2127
     * Show a search form on this page?
2128
     *
2129
     * @return bool
2130
     */
2131
    public function ShowSearchFormAtAll()
2132
    {
2133
        return true;
2134
    }
2135
2136
    /**
2137
     * Is the current page a display of search results.
2138
     *
2139
     * This does not mean that something is actively being search for,
2140
     * it could also be just "showing the search results"
2141
     *
2142
     * @return bool
2143
     */
2144
    public function IsSearchResults()
2145
    {
2146
        return $this->isSearchResults;
2147
    }
2148
2149
    /**
2150
     * Is there something actively being searched for?
2151
     *
2152
     * This is different from IsSearchResults.
2153
     *
2154
     * @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...
2155
     */
2156
    public function ActiveSearchTerm()
2157
    {
2158
        $data = Session::get(Config::inst()->get('ProductSearchForm', 'form_data_session_variable'));
2159
        if (!empty($data['Keyword'])) {
2160
            return $this->IsSearchResults();
2161
        }
2162
    }
2163
2164
    /****************************************************
2165
     *  Filter / Sort / Display related controllers
2166
    /****************************************************/
2167
2168
    /**
2169
     * Do we show all products on one page?
2170
     *
2171
     * @return bool
2172
     */
2173
    public function ShowFiltersAndDisplayLinks()
2174
    {
2175
        if ($this->TotalCountGreaterThanOne()) {
2176
            if ($this->HasFilters()) {
2177
                return true;
2178
            }
2179
            if ($this->DisplayLinks()) {
2180
                return true;
2181
            }
2182
        }
2183
2184
        return false;
2185
    }
2186
2187
    /**
2188
     * Do we show the sort links.
2189
     *
2190
     * A bit arbitrary to say three,
2191
     * but there is not much point to sort three or less products
2192
     *
2193
     * @return bool
2194
     */
2195
    public function ShowSortLinks($minimumCount = 3)
2196
    {
2197
        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...
2198
            return true;
2199
        }
2200
2201
        return false;
2202
    }
2203
2204
    /**
2205
     * Is there a special filter operating at the moment?
2206
     * Is the current filter the default one (return inverse!)?
2207
     *
2208
     * @return bool
2209
     */
2210
    public function HasFilter()
2211
    {
2212
        return $this->getCurrentUserPreferences('FILTER') != $this->getMyUserPreferencesDefault('FILTER')
2213
        || $this->filterForGroupObject;
2214
    }
2215
2216
    /**
2217
     * Is there a special sort operating at the moment?
2218
     * Is the current sort the default one (return inverse!)?
2219
     *
2220
     * @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...
2221
     */
2222
    public function HasSort()
2223
    {
2224
        $sort = $this->getCurrentUserPreferences('SORT');
2225
        if ($sort != $this->getMyUserPreferencesDefault('SORT')) {
2226
            return true;
2227
        }
2228
    }
2229
2230
    /**
2231
     * @return boolean
2232
     */
2233
    public function HasFilterOrSort()
2234
    {
2235
        return $this->HasFilter() || $this->HasSort();
2236
    }
2237
2238
    /**
2239
     * @return boolean
2240
     */
2241
    public function HasFilterOrSortFullList()
2242
    {
2243
        return $this->HasFilterOrSort() || $this->IsShowFullList();
2244
    }
2245
2246
    /**
2247
     * are filters available?
2248
     * we check one at the time so that we do the least
2249
     * amount of DB queries.
2250
     *
2251
     * @return bool
2252
     */
2253
    public function HasFilters()
2254
    {
2255
        $countFilters = $this->FilterLinks()->count();
2256
        if ($countFilters > 1) {
2257
            return true;
2258
        }
2259
        $countGroupFilters = $this->ProductGroupFilterLinks()->count();
2260
        if ($countGroupFilters > 1) {
2261
            return true;
2262
        }
2263
        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...
2264
            return true;
2265
        }
2266
2267
        return false;
2268
    }
2269
2270
    /**
2271
     * Do we show all products on one page?
2272
     *
2273
     * @return bool
2274
     */
2275
    public function IsShowFullList()
2276
    {
2277
        return $this->showFullList;
2278
    }
2279
2280
    /**
2281
     * returns the current filter applied to the list
2282
     * in a human readable string.
2283
     *
2284
     * @return string
2285
     */
2286
    public function CurrentDisplayTitle()
2287
    {
2288
        $displayKey = $this->getCurrentUserPreferences('DISPLAY');
2289
        if ($displayKey != $this->getMyUserPreferencesDefault('DISPLAY')) {
2290
            return $this->getUserPreferencesTitle('DISPLAY', $displayKey);
2291
        }
2292
    }
2293
2294
    /**
2295
     * returns the current filter applied to the list
2296
     * in a human readable string.
2297
     *
2298
     * @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...
2299
     */
2300
    public function CurrentFilterTitle()
2301
    {
2302
        $filterKey = $this->getCurrentUserPreferences('FILTER');
2303
        $filters = array();
2304
        if ($filterKey != $this->getMyUserPreferencesDefault('FILTER')) {
2305
            $filters[] = $this->getUserPreferencesTitle('FILTER', $filterKey);
2306
        }
2307
        if ($this->filterForGroupObject) {
2308
            $filters[] = $this->filterForGroupObject->MenuTitle;
2309
        }
2310
        if (count($filters)) {
2311
            return implode(', ', $filters);
2312
        }
2313
    }
2314
2315
    /**
2316
     * returns the current sort applied to the list
2317
     * in a human readable string.
2318
     *
2319
     * @return string
2320
     */
2321
    public function CurrentSortTitle()
2322
    {
2323
        $sortKey = $this->getCurrentUserPreferences('SORT');
2324
        if ($sortKey != $this->getMyUserPreferencesDefault('SORT')) {
2325
            return $this->getUserPreferencesTitle('SORT', $sortKey);
2326
        }
2327
    }
2328
2329
    /**
2330
     * short-cut for getMyUserPreferencesDefault("DISPLAY")
2331
     * for use in templtes.
2332
     *
2333
     * @return string - key
2334
     */
2335
    public function MyDefaultDisplayStyle()
2336
    {
2337
        return $this->getMyUserPreferencesDefault('DISPLAY');
2338
    }
2339
2340
    /**
2341
     * Number of entries per page limited by total number of pages available...
2342
     *
2343
     * @return int
2344
     */
2345
    public function MaxNumberOfProductsPerPage()
2346
    {
2347
        return $this->MyNumberOfProductsPerPage() > $this->TotalCount() ? $this->TotalCount() : $this->MyNumberOfProductsPerPage();
2348
    }
2349
2350
    /****************************************************
2351
     *  TEMPLATE METHODS FILTER LINK
2352
    /****************************************************/
2353
2354
    /**
2355
     * Provides a ArrayList of links for filters products.
2356
     *
2357
     * @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...
2358
     */
2359
    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...
2360
    {
2361
        $cacheKey = 'FilterLinks_'.($this->filterForGroupObject ? $this->filterForGroupObject->ID : 0);
2362
        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...
2363
            //do nothing
2364
        } else {
2365
            $list = $this->userPreferencesLinks('FILTER');
2366
            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...
2367
                $key = $obj->SelectKey;
2368
                if ($key != $this->getMyUserPreferencesDefault('FILTER')) {
2369
                    $count = count($this->currentInitialProductsAsCachedArray($key));
2370
                    if ($count == 0) {
2371
                        $list->remove($obj);
2372
                    } else {
2373
                        $obj->Count = $count;
2374
                    }
2375
                }
2376
            }
2377
            $this->saveObjectStore($list, $cacheKey);
2378
        }
2379
        $selectedItem = $this->getCurrentUserPreferences('FILTER');
2380
        foreach ($list as $obj) {
2381
            $canHaveCurrent = true;
2382
            if ($this->filterForGroupObject) {
2383
                $canHaveCurrent = false;
2384
            }
2385
            $obj->Current = $selectedItem == $obj->SelectKey && $canHaveCurrent ? true : false;
2386
            $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2387
            $obj->Ajaxify = true;
2388
        }
2389
2390
        return $list;
2391
    }
2392
2393
    /**
2394
     * returns a list of items (with links).
2395
     *
2396
     * @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...
2397
     */
2398
    public function ProductGroupFilterLinks()
2399
    {
2400
        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...
2401
            //do nothing
2402
        } else {
2403
            $arrayOfItems = array();
2404
2405
            $baseArray = $this->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2406
2407
            //also show
2408
            $items = $this->ProductGroupsFromAlsoShowProducts();
2409
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2410
            //also show inverse
2411
            $items = $this->ProductGroupsFromAlsoShowProductsInverse();
2412
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2413
2414
            //parent groups
2415
            $items = $this->ProductGroupsParentGroups();
2416
            $arrayOfItems = array_merge($arrayOfItems, $this->productGroupFilterLinksCount($items, $baseArray, true));
2417
2418
            //child groups
2419
            $items = $this->MenuChildGroups();
2420
            $arrayOfItems = array_merge($arrayOfItems,  $this->productGroupFilterLinksCount($items, $baseArray, true));
2421
2422
            ksort($arrayOfItems);
2423
            $array = array();
2424
            foreach ($arrayOfItems as $arrayOfItem) {
2425
                $array[] = $this->makeArrayItem($arrayOfItem);
2426
            }
2427
            $this->saveObjectStore($array, 'ProductGroupFilterLinks');
2428
        }
2429
        $arrayList = ArrayList::create();
2430
        foreach ($array as $item) {
2431
            $arrayList->push(ArrayData::create($item));
2432
        }
2433
        return $arrayList;
2434
    }
2435
2436
    /**
2437
     * counts the total number in the combination....
2438
     *
2439
     * @param DataList $items     - list of
2440
     * @param Arary    $baseArray - list of products on the current page
2441
     *
2442
     * @return array
2443
     */
2444
    protected function productGroupFilterLinksCount($items, $baseArray, $ajaxify = true)
2445
    {
2446
        $array = array();
2447
        if ($items && $items->count()) {
2448
            foreach ($items as $item) {
2449
                $arrayOfIDs = $item->currentInitialProductsAsCachedArray($this->getMyUserPreferencesDefault('FILTER'));
2450
                $newArray = array_intersect_key(
2451
                    $arrayOfIDs,
2452
                    $baseArray
2453
                );
2454
                $count = count($newArray);
2455
                if ($count) {
2456
                    $array[$item->Title] = array(
2457
                        'Item' => $item,
2458
                        'Count' => $count,
2459
                        'Ajaxify' => $ajaxify,
2460
                    );
2461
                }
2462
            }
2463
        }
2464
2465
        return $array;
2466
    }
2467
2468
    /**
2469
     * @param array itemInArray (Item, Count, UserFilterAction)
2470
     *
2471
     * @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...
2472
     */
2473
    protected function makeArrayItem($itemInArray)
2474
    {
2475
        $item = $itemInArray['Item'];
2476
        $count = $itemInArray['Count'];
2477
        $ajaxify = $itemInArray['Ajaxify'];
2478
        $filterForGroupObjectID = $this->filterForGroupObject ? $this->filterForGroupObject->ID : 0;
2479
        $isCurrent = $item->ID == $filterForGroupObjectID;
2480
        if ($ajaxify) {
2481
            $link = $this->Link('filterforgroup/'.$item->URLSegment);
2482
        } else {
2483
            $link = $item->Link();
2484
        }
2485
        return array(
2486
            'Title' => $item->Title,
2487
            'Count' => $count,
2488
            'SelectKey' => $item->URLSegment,
2489
            'Current' => $isCurrent ? true : false,
2490
            'MyLinkingMode' => $isCurrent ? 'current' : 'link',
2491
            'FilterLink' => $link,
2492
            'Ajaxify' => $ajaxify ? true : false,
2493
        );
2494
    }
2495
2496
    /**
2497
     * Provides a ArrayList of links for sorting products.
2498
     */
2499
    public function SortLinks()
2500
    {
2501
        $list = $this->userPreferencesLinks('SORT');
2502
        $selectedItem = $this->getCurrentUserPreferences('SORT');
2503
        if ($list) {
2504
            foreach ($list as $obj) {
2505
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2506
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2507
                $obj->Ajaxify = true;
2508
            }
2509
2510
            return $list;
2511
        }
2512
    }
2513
2514
    /**
2515
     * Provides a ArrayList for displaying display links.
2516
     */
2517
    public function DisplayLinks()
2518
    {
2519
        $list = $this->userPreferencesLinks('DISPLAY');
2520
        $selectedItem = $this->getCurrentUserPreferences('DISPLAY');
2521
        if ($list) {
2522
            foreach ($list as $obj) {
2523
                $obj->Current = $selectedItem == $obj->SelectKey ? true : false;
2524
                $obj->LinkingMode = $obj->Current ? 'current' : 'link';
2525
                $obj->Ajaxify = true;
2526
            }
2527
2528
            return $list;
2529
        }
2530
    }
2531
2532
    /**
2533
     * Link that returns a list of all the products
2534
     * for this product group as a simple list.
2535
     *
2536
     * @return string
2537
     */
2538
    public function ListAllLink()
2539
    {
2540
        if ($this->filterForGroupObject) {
2541
            return $this->Link('filterforgroup/'.$this->filterForGroupObject->URLSegment).'?showfulllist=1';
2542
        } else {
2543
            return $this->Link().'?showfulllist=1';
2544
        }
2545
    }
2546
2547
    /**
2548
     * Link that returns a list of all the products
2549
     * for this product group as a simple list.
2550
     *
2551
     * @return string
2552
     */
2553
    public function ListAFewLink()
2554
    {
2555
        return str_replace('?showfulllist=1', '', $this->ListAllLink());
2556
    }
2557
2558
    /**
2559
     * Link that returns a list of all the products
2560
     * for this product group as a simple list.
2561
     *
2562
     * It resets everything - not just filter....
2563
     *
2564
     * @return string
2565
     */
2566
    public function ResetPreferencesLink($escapedAmpersands = true)
2567
    {
2568
        $ampersand = '&';
2569
        if ($escapedAmpersands) {
2570
            $ampersand = '&amp;';
2571
        }
2572
        $getVariableNameFilter = $this->getSortFilterDisplayNames('FILTER', 'getVariable');
2573
        $getVariableNameSort = $this->getSortFilterDisplayNames('SORT', 'getVariable');
2574
2575
        return $this->Link().'?'.
2576
            $getVariableNameFilter.'='.$this->getMyUserPreferencesDefault('FILTER').$ampersand.
2577
            $getVariableNameSort.'='.$this->getMyUserPreferencesDefault('SORT').$ampersand.
2578
            'reload=1';
2579
    }
2580
2581
    /**
2582
     * Link to the search results.
2583
     *
2584
     * @return string
2585
     */
2586
    public function SearchResultLink()
2587
    {
2588
        if ($this->HasSearchResults() && !$this->isSearchResults) {
2589
            return $this->Link('searchresults');
2590
        }
2591
    }
2592
2593
    /****************************************************
2594
     *  INTERNAL PROCESSING: PRODUCT LIST
2595
    /****************************************************/
2596
2597
    /**
2598
     * turns full list into paginated list.
2599
     *
2600
     * @param SS_List
2601
     *
2602
     * @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...
2603
     */
2604
    protected function paginateList(SS_List $list)
2605
    {
2606
        if ($list && $list->count()) {
2607
            if ($this->IsShowFullList()) {
2608
                $obj = PaginatedList::create($list, $this->request);
2609
                $obj->setPageLength(EcommerceConfig::get('ProductGroup', 'maximum_number_of_products_to_list') + 1);
2610
2611
                return $obj;
2612
            } else {
2613
                $obj = PaginatedList::create($list, $this->request);
2614
                $obj->setPageLength($this->MyNumberOfProductsPerPage());
2615
2616
                return $obj;
2617
            }
2618
        }
2619
    }
2620
2621
    /****************************************************
2622
     *  INTERNAL PROCESSING: USER PREFERENCES
2623
    /****************************************************/
2624
2625
    /**
2626
     * Checks out a bunch of $_GET variables
2627
     * that are used to work out user preferences
2628
     * Some of these are saved to session.
2629
     *
2630
     * @param array $overrideArray - override $_GET variable settings
2631
     */
2632
    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...
2633
    {
2634
2635
        //save sort - filter - display
2636
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2637
        foreach ($sortFilterDisplayNames as $type => $oneTypeArray) {
2638
            $getVariableName = $oneTypeArray['getVariable'];
2639
            $sessionName = $oneTypeArray['sessionName'];
2640
            if (isset($overrideArray[$getVariableName])) {
2641
                $newPreference = $overrideArray[$getVariableName];
2642
            } else {
2643
                $newPreference = $this->request->getVar($getVariableName);
2644
            }
2645
            if ($newPreference) {
2646
                $optionsVariableName = $oneTypeArray['configName'];
2647
                $options = EcommerceConfig::get($this->ClassName, $optionsVariableName);
2648
                if (isset($options[$newPreference])) {
2649
                    Session::set('ProductGroup_'.$sessionName, $newPreference);
2650
                    //save in model as well...
2651
                }
2652
            } else {
2653
                $newPreference = Session::get('ProductGroup_'.$sessionName);
2654
            }
2655
            //save data in model...
2656
            $this->setCurrentUserPreference($type, $newPreference);
2657
        }
2658
        /* 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...
2659
        $this->setCurrentUserPreference(
2660
            "URLSegments",
2661
            array(
2662
                "Action" => $this->request->param("Action"),
2663
                "ID" => $this->request->param("ID")
2664
            )
2665
        );
2666
        */
2667
2668
        //clearing data..
2669
        if ($this->request->getVar('reload')) {
2670
            //reset other session variables...
2671
            Session::set($this->SearchResultsSessionVariable(false), '');
2672
            Session::set($this->SearchResultsSessionVariable(true), '');
2673
2674
            return $this->redirect($this->Link());
2675
        }
2676
2677
        //full list ....
2678
        if ($this->request->getVar('showfulllist')) {
2679
            $this->showFullList = true;
2680
        }
2681
    }
2682
2683
    /**
2684
     * Checks for the most applicable user preferences for this user:
2685
     * 1. session value
2686
     * 2. getMyUserPreferencesDefault.
2687
     *
2688
     * @param string $type - FILTER | SORT | DISPLAY
2689
     *
2690
     * @return string
2691
     *
2692
     * @todo: move to controller?
2693
     */
2694
    protected function getCurrentUserPreferences($type)
2695
    {
2696
        $sessionName = $this->getSortFilterDisplayNames($type, 'sessionName');
2697
        if ($sessionValue = Session::get('ProductGroup_'.$sessionName)) {
2698
            $key = Convert::raw2sql($sessionValue);
2699
        } else {
2700
            $key = $this->getMyUserPreferencesDefault($type);
2701
        }
2702
2703
        return $key;
2704
    }
2705
2706
    /**
2707
     * Provides a dataset of links for a particular user preference.
2708
     *
2709
     * @param string $type SORT | FILTER | DISPLAY - e.g. sort_options
2710
     *
2711
     * @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...
2712
     */
2713
    protected function userPreferencesLinks($type)
2714
    {
2715
        //get basics
2716
        $sortFilterDisplayNames = $this->getSortFilterDisplayNames();
2717
        $options = $this->getConfigOptions($type);
2718
2719
        //if there is only one option then do not bother
2720
        if (count($options) < 2) {
2721
            return;
2722
        }
2723
2724
        //get more config names
2725
        $translationCode = $sortFilterDisplayNames[$type]['translationCode'];
2726
        $getVariableName = $sortFilterDisplayNames[$type]['getVariable'];
2727
        $arrayList = ArrayList::create();
2728
        if (count($options)) {
2729
            foreach ($options as $key => $array) {
2730
                //$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...
2731
2732
                $link = '?'.$getVariableName."=$key";
2733
                if ($type == 'FILTER') {
2734
                    $link = $this->Link().$link;
2735
                } else {
2736
                    $link = $this->request->getVar('url').$link;
2737
                }
2738
                $arrayList->push(ArrayData::create(array(
2739
                    'Name' => _t('ProductGroup.'.$translationCode.strtoupper(str_replace(' ', '', $array['Title'])), $array['Title']),
2740
                    'Link' => $link,
2741
                    'SelectKey' => $key,
2742
                    //we add current at runtime, so we can store the object without current set...
2743
                    //'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...
2744
                    //'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...
2745
                )));
2746
            }
2747
        }
2748
2749
        return $arrayList;
2750
    }
2751
2752
    /****************************************************
2753
     *  INTERNAL PROCESSING: TITLES
2754
    /****************************************************/
2755
2756
    /**
2757
     * variable to make sure secondary title only gets
2758
     * added once.
2759
     *
2760
     * @var bool
2761
     */
2762
    protected $secondaryTitleHasBeenAdded = false;
2763
2764
    /**
2765
     * add a secondary title to the main title
2766
     * in case there is, for example, a filter applied
2767
     * e.g. Socks | MyBrand.
2768
     *
2769
     * @param string
2770
     */
2771
    protected function addSecondaryTitle($secondaryTitle = '')
2772
    {
2773
        $pipe = _t('ProductGroup.TITLE_SEPARATOR', ' | ');
2774
        if (! $this->secondaryTitleHasBeenAdded) {
2775
            if (trim($secondaryTitle)) {
2776
                $secondaryTitle = $pipe.$secondaryTitle;
2777
            }
2778
            if ($this->IsSearchResults()) {
2779
                if ($array = $this->searchResultsArrayFromSession()) {
2780
                    //we remove 1 item here, because the array starts with 0 => 0
2781
                    $count = count($array) - 1;
2782
                    if ($count > 3) {
2783
                        $toAdd = $count. ' '._t('ProductGroup.PRODUCTS_FOUND', 'Products Found');
2784
                        $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2785
                    }
2786
                } else {
2787
                    $toAdd = _t('ProductGroup.SEARCH_RESULTS', 'Search Results');
2788
                    $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2789
                }
2790
            }
2791
            if (is_object($this->filterForGroupObject)) {
2792
                $toAdd = $this->filterForGroupObject->Title;
2793
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2794
            }
2795
            if ($this->IsShowFullList()) {
2796
                $toAdd = _t('ProductGroup.LIST_VIEW', 'List View');
2797
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2798
            }
2799
            $filter = $this->getCurrentUserPreferences('FILTER');
2800
            if ($filter != $this->getMyUserPreferencesDefault('FILTER')) {
2801
                $toAdd = $this->getUserPreferencesTitle('FILTER', $this->getCurrentUserPreferences('FILTER'));
2802
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2803
            }
2804
            if ($this->HasSort()) {
2805
                $toAdd = $this->getUserPreferencesTitle('SORT', $this->getCurrentUserPreferences('SORT'));
2806
                $secondaryTitle .= $this->cleanSecondaryTitleForAddition($pipe, $toAdd);
2807
            }
2808
            if ($secondaryTitle) {
2809
                $this->Title .= $secondaryTitle;
2810
                if (isset($this->MetaTitle)) {
2811
                    $this->MetaTitle .= $secondaryTitle;
2812
                }
2813
            }
2814
            //dont update menu title, because the entry in the menu
2815
            //should stay the same as it links back to the unfiltered
2816
            //page (in some cases).
2817
            $this->secondaryTitleHasBeenAdded = true;
2818
        }
2819
    }
2820
2821
    /**
2822
     * removes any spaces from the 'toAdd' bit and adds the pipe if there is
2823
     * anything to add at all.  Through the lang files, you can change the pipe
2824
     * symbol to anything you like.
2825
     *
2826
     * @param  string $pipe
2827
     * @param  string $toAdd
2828
     * @return string
2829
     */
2830
    protected function cleanSecondaryTitleForAddition($pipe, $toAdd)
2831
    {
2832
        $toAdd = trim($toAdd);
2833
        $length = strlen($toAdd);
2834
        if ($length > 0) {
2835
            $toAdd = $pipe.$toAdd;
2836
        }
2837
        return $toAdd;
2838
    }
2839
2840
    /****************************************************
2841
     *  DEBUG
2842
    /****************************************************/
2843
2844
    public function debug()
2845
    {
2846
        $member = Member::currentUser();
2847
        if (!$member || !$member->IsShopAdmin()) {
2848
            $messages = array(
2849
                'default' => 'You must login as an admin to use debug functions.',
2850
            );
2851
            Security::permissionFailure($this, $messages);
2852
        }
2853
        $this->ProductsShowable();
2854
        $html = EcommerceTaskDebugCart::debug_object($this->dataRecord);
2855
        $html .= '<ul>';
2856
2857
        $html .= '<li><hr /><h3>Available options</h3><hr /></li>';
2858
        $html .= '<li><b>Sort Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('SORT'), 1).'</pre> </li>';
2859
        $html .= '<li><b>Filter Options for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('FILTER'), 1).'</pre></li>';
2860
        $html .= '<li><b>Display Styles for Dropdown:</b><pre> '.print_r($this->getUserPreferencesOptionsForDropdown('DISPLAY'), 1).'</pre> </li>';
2861
2862
        $html .= '<li><hr /><h3>Selection Setting (what is set as default for this page)</h3><hr /></li>';
2863
        $html .= '<li><b>MyDefaultFilter:</b> '.$this->getMyUserPreferencesDefault('FILTER').' </li>';
2864
        $html .= '<li><b>MyDefaultSortOrder:</b> '.$this->getMyUserPreferencesDefault('SORT').' </li>';
2865
        $html .= '<li><b>MyDefaultDisplayStyle:</b> '.$this->getMyUserPreferencesDefault('DISPLAY').' </li>';
2866
        $html .= '<li><b>MyNumberOfProductsPerPage:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
2867
        $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>';
2868
2869
        $html .= '<li><hr /><h3>Current Settings</h3><hr /></li>';
2870
        $html .= '<li><b>Current Sort Order:</b> '.$this->getCurrentUserPreferences('SORT').' </li>';
2871
        $html .= '<li><b>Current Filter:</b> '.$this->getCurrentUserPreferences('FILTER').' </li>';
2872
        $html .= '<li><b>Current display style:</b> '.$this->getCurrentUserPreferences('DISPLAY').' </li>';
2873
2874
        $html .= '<li><hr /><h3>DATALIST: totals, numbers per page etc</h3><hr /></li>';
2875
        $html .= '<li><b>Total number of products:</b> '.$this->TotalCount().' </li>';
2876
        $html .= '<li><b>Is there more than one product:</b> '.($this->TotalCountGreaterThanOne() ? 'YES' : 'NO').' </li>';
2877
        $html .= '<li><b>Number of products per page:</b> '.$this->MyNumberOfProductsPerPage().' </li>';
2878
2879
        $html .= '<li><hr /><h3>SQL Factors</h3><hr /></li>';
2880
        $html .= '<li><b>Default sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT'), 1).' </li>';
2881
        $html .= '<li><b>User sort SQL:</b> '.print_r($this->getUserSettingsOptionSQL('SORT',  $this->getCurrentUserPreferences('SORT')), 1).' </li>';
2882
        $html .= '<li><b>Default Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER'), 1).'</pre> </li>';
2883
        $html .= '<li><b>User Filter SQL:</b> <pre>'.print_r($this->getUserSettingsOptionSQL('FILTER',  $this->getCurrentUserPreferences('FILTER')), 1).'</pre> </li>';
2884
        $html .= '<li><b>Buyable Class name:</b> '.$this->getBuyableClassName().' </li>';
2885
        $html .= '<li><b>allProducts:</b> '.print_r(str_replace('"', '`', $this->allProducts->sql()), 1).' </li>';
2886
2887
        $html .= '<li><hr /><h3>Search</h3><hr /></li>';
2888
        $resultArray = $this->searchResultsArrayFromSession();
2889
        $productGroupArray = explode(',', Session::get($this->SearchResultsSessionVariable(true)));
2890
        $html .= '<li><b>Is Search Results:</b> '.($this->IsSearchResults() ? 'YES' : 'NO').' </li>';
2891
        $html .= '<li><b>Products In Search (session variable : '.$this->SearchResultsSessionVariable(false).'):</b> '.print_r($resultArray, 1).' </li>';
2892
        $html .= '<li><b>Product Groups In Search (session variable : '.$this->SearchResultsSessionVariable(true).'):</b> '.print_r($productGroupArray, 1).' </li>';
2893
2894
        $html .= '<li><hr /><h3>Other</h3><hr /></li>';
2895
        if ($image = $this->BestAvailableImage()) {
2896
            $html .= '<li><b>Best Available Image:</b> <img src="'.$image->Link.'" /> </li>';
2897
        }
2898
        $html .= '<li><b>BestAvailableImage:</b> '.($this->BestAvailableImage() ? $this->BestAvailableImage()->Link : 'no image available').' </li>';
2899
        $html .= '<li><b>Is this an ecommerce page:</b> '.($this->IsEcommercePage() ? 'YES' : 'NO').' </li>';
2900
        $html .= '<li><hr /><h3>Related Groups</h3><hr /></li>';
2901
        $html .= '<li><b>Parent product group:</b> '.($this->ParentGroup() ? $this->ParentGroup()->Title : '[NO PARENT GROUP]').'</li>';
2902
2903
        $childGroups = $this->ChildGroups(99);
2904
        if ($childGroups->count()) {
2905
            $childGroups = $childGroups->map('ID', 'MenuTitle');
2906
            $html .= '<li><b>Child Groups (all):</b><pre> '.print_r($childGroups, 1).' </pre></li>';
2907
        } else {
2908
            $html .= '<li><b>Child Groups (full tree): </b>NONE</li>';
2909
        }
2910
        $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>';
2911
        $html .= '<li><b>the inverse of ProductGroupsFromAlsoShowProducts:</b><pre> '.print_r($this->ProductGroupsFromAlsoShowProductsInverse()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
2912
        $html .= '<li><b>all product parent groups:</b><pre> '.print_r($this->ProductGroupsParentGroups()->map('ID', 'Title')->toArray(), 1).' </pre></li>';
2913
2914
        $html .= '<li><hr /><h3>Product Example and Links</h3><hr /></li>';
2915
        $product = Product::get()->filter(array('ParentID' => $this->ID))->first();
2916
        if ($product) {
2917
            $html .= '<li><b>Product View:</b> <a href="'.$product->Link().'">'.$product->Title.'</a> </li>';
2918
            $html .= '<li><b>Product Debug:</b> <a href="'.$product->Link('debug').'">'.$product->Title.'</a> </li>';
2919
            $html .= '<li><b>Product Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$product->ID.'">'.$product->Title.'</a> </li>';
2920
            $html .= '<li><b>ProductGroup Admin Page:</b> <a href="'.'/admin/pages/edit/show/'.$this->ID.'">'.$this->Title.'</a> </li>';
2921
        } else {
2922
            $html .= '<li>this page has no products of its own</li>';
2923
        }
2924
        $html .= '</ul>';
2925
2926
        return $html;
2927
    }
2928
}
2929