ShoppingCartFactory::addItem()   F
last analyzed

Complexity

Conditions 17
Paths 385

Size

Total Lines 95
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 17
eloc 49
c 3
b 1
f 0
nc 385
nop 2
dl 0
loc 95
rs 2.0708

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
namespace SilverCommerce\ShoppingCart;
4
5
use SilverStripe\Dev\Debug;
6
use SilverStripe\Control\Cookie;
7
use SilverStripe\Security\Security;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\SiteConfig\SiteConfig;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\ValidationException;
12
use SilverStripe\Core\Config\Configurable;
13
use SilverStripe\Core\Injector\Injectable;
14
use SilverStripe\ORM\FieldType\DBDatetime;
15
use SilverCommerce\OrdersAdmin\Model\LineItem;
16
use SilverCommerce\OrdersAdmin\Model\LineItemCustomisation;
17
use SilverCommerce\ShoppingCart\Tasks\CleanExpiredEstimatesTask;
18
use SilverCommerce\ShoppingCart\Model\ShoppingCart as ShoppingCartModel;
19
use SilverCommerce\ShoppingCart\Control\ShoppingCart as ShoppingCartController;
20
21
/**
22
 * Factory to handle setting up and interacting with a ShoppingCart
23
 * object.
24
 * 
25
 */
26
class ShoppingCartFactory
27
{
28
    use Injectable;
29
    use Configurable;
30
31
    /**
32
     * Name of the test cookie used to check if cookies are allowed
33
     */
34
    const TEST_COOKIE = "ShoppingCartFactoryTest";
35
36
    /**
37
     * Name of Cookie/Session used to track cart access key 
38
     */
39
    const COOKIE_NAME = "ShoppingCart.Key";
40
41
    /**
42
     * The default class that is used by the factroy
43
     * 
44
     * @var string
45
     */
46
    private static $model = ShoppingCartModel::class;
47
48
    /**
49
     * The default class that is used by the factroy
50
     * 
51
     * @var string
52
     */
53
    private static $controller = ShoppingCartController::class;
0 ignored issues
show
introduced by
The private property $controller is not used, and could be removed.
Loading history...
54
55
    /**
56
     * Should the cart globally check for stock levels on items added?
57
     * Using this setting will ignore individual "Stocked" settings
58
     * on Shopping Cart Items.
59
     *
60
     * @var string
61
     */
62
    private static $check_stock_levels = false;
63
64
    /**
65
     * whether or not the cleaning task should be left to a cron job
66
     *
67
     * @var boolean
68
     * @config
69
     */
70
    private static $cron_cleaner = false;
71
72
    /**
73
     * Allow the user to add multiple discounts to the cart
74
     * 0 = unlimited
75
     *
76
     * @var int
77
     * @config
78
     */
79
    private static $discount_limit = 1;
0 ignored issues
show
introduced by
The private property $discount_limit is not used, and could be removed.
Loading history...
80
81
    /**
82
     * The current shopping cart
83
     * 
84
     * @var ShoppingCart
85
     */
86
    protected $current;
87
88
    /**
89
     * Setup the shopping cart and return an instance
90
     * 
91
     * @return ShoppingCart
92
     **/ 
93
    public function __construct()
94
    {
95
        $cookies = $this->cookiesSupported();
0 ignored issues
show
Unused Code introduced by
The assignment to $cookies is dead and can be removed.
Loading history...
96
        $member = Security::getCurrentUser();
97
98
        $cart = $this->findOrMakeCart();
99
100
        // If we don't have any discounts, a user is logged in and he has
101
        // access to discounts through a group, add the discount here
102
        if (!$cart->Discounts()->Count() > 0 && $member && $member->getDiscount()) {
0 ignored issues
show
Bug introduced by
The method Discounts() does not exist on SilverCommerce\ShoppingCart\Model\ShoppingCart. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

102
        if (!$cart->/** @scrutinizer ignore-call */ Discounts()->Count() > 0 && $member && $member->getDiscount()) {
Loading history...
103
            $discount = $member->getDiscount();
104
            if ($discount->exists()) {
105
                $cart->addDiscount($discount);
0 ignored issues
show
Bug introduced by
The method addDiscount() does not exist on SilverCommerce\ShoppingCart\Model\ShoppingCart. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

105
                $cart->/** @scrutinizer ignore-call */ 
106
                       addDiscount($discount);
Loading history...
106
            }
107
        }
108
109
        if (!$this->config()->cron_cleaner) {
110
            $this->cleanOld();
111
        }
112
113
        $this->current = $cart;
0 ignored issues
show
Documentation Bug introduced by
It seems like $cart of type SilverCommerce\ShoppingCart\Model\ShoppingCart is incompatible with the declared type SilverCommerce\ShoppingCart\ShoppingCart of property $current.

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...
114
    }
115
116
    /**
117
     * Get the current session from the current request
118
     * 
119
     * @return Session
0 ignored issues
show
Bug introduced by
The type SilverCommerce\ShoppingCart\Session was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
120
     */
121
    public function getSession()
122
    {
123
        $request = Injector::inst()->get(HTTPRequest::class);
124
        return $request->getSession();
125
    }
126
127
    /**
128
     * Either find an existing cart, or create a new one.
129
     * 
130
     * @return ShoppingCartModel
131
     */
132
    public function findOrMakeCart()
133
    {
134
        $cookies = $this->cookiesSupported();
135
        $session = $this->getSession();
136
        $classname = self::config()->model;
137
        $cart = null;
138
        $write = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $write is dead and can be removed.
Loading history...
139
        $member = Security::getCurrentUser();
140
141
        if ($cookies) {
142
            $cart_id = Cookie::get(self::COOKIE_NAME);
143
        } else {
144
            $cart_id = $session->get(self::COOKIE_NAME);
145
        }
146
147
        // Try to get a cart from the the DB
148
        if (isset($cart_id)) {
149
            $cart = $classname::get()->find('AccessKey', $cart_id);
150
        }
151
152
        // Does the current member have a cart?
153
        if (empty($cart) && isset($member) && $member->getCart()) {
154
            $cart = $member->getCart();
155
        }
156
157
        // Finally, if nothing is set, create a new instance to return
158
        if (empty($cart)) {
159
            $cart = $classname::create();
160
        }
161
162
        return $cart;
163
    }
164
165
    /**
166
     * Run the task to clean old shopping carts
167
     * 
168
     * @return null 
169
     */
170
    public function cleanOld()
171
    {
172
        $siteconfig = SiteConfig::current_site_config();
173
        $date = $siteconfig->dbobject("LastEstimateClean");
174
        $request = Injector::inst()->get(HTTPRequest::class);
175
176
        if (!$date || ($date && !$date->IsToday())) {
0 ignored issues
show
introduced by
$date is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
177
            $task = Injector::inst()->create(CleanExpiredEstimatesTask::class);
178
            $task->setSilent(true);
179
            $task->run($request);
180
            $siteconfig->LastEstimateClean = DBDatetime::now()->Value;
181
            $siteconfig->write();
182
        }
183
    }
184
185
    /**
186
     * Test to see if the current user supports cookies
187
     * 
188
     * @return boolean
189
     */
190
    public function cookiesSupported()
191
    {
192
        Cookie::set(self::TEST_COOKIE, 1);
193
        $cookie = Cookie::get(self::TEST_COOKIE);
194
        Cookie::force_expiry(self::TEST_COOKIE);
195
196
        return (empty($cookie)) ? false : true;
197
    }
198
199
    /**
200
     * Get the current shopping cart
201
     * 
202
     * @return ShoppingCart
203
     */ 
204
    public function getCurrent()
205
    {
206
        return $this->current;
207
    }
208
209
    /**
210
     * Add an item to the shopping cart. By default this should be a
211
     * line item, but this method will determine if the correct object
212
     * has been provided before attempting to add.
213
     *
214
     * @param array $item The item to add (defaults to @link LineItem)
215
     * @param array $customisations (A list of @LineItemCustomisations customisations to provide)
216
     * 
217
     * @throws ValidationException
218
     * @return self
219
     */
220
    public function addItem($item, $customisations = [])
221
    {
222
        $cart = $this->getCurrent();
223
        $stock_item = $item->FindStockItem();
224
        $added = false;
225
226
        if (!$item instanceof LineItem) {
0 ignored issues
show
introduced by
$item is never a sub-type of SilverCommerce\OrdersAdmin\Model\LineItem.
Loading history...
227
            throw new ValidationException(_t(
228
                "ShoppingCart.WrongItemClass",
229
                "Item needs to be of class {class}",
230
                ["class" => LineItem::class]
231
            ));
232
        }
233
234
        // Start off by writing our item object (if it is
235
        // not in the DB)
236
        if (!$item->exists()) {
237
            $item->write();
238
        }
239
240
        if (!is_array($customisations)) {
241
            $customisations = [$customisations];
242
        }
243
244
        // Find any item customisation associations
245
        $custom_association = null;
246
        $custom_associations = array_merge(
247
            $item->hasMany(),
248
            $item->manyMany()
249
        );
250
251
        // Define association of item to customisations
252
        foreach ($custom_associations as $key => $value) {
253
            $class = $value::create();
254
            if ($class instanceof LineItemCustomisation) {
255
                $custom_association = $key;
256
                break;
257
            }
258
        }
259
260
261
        // Map any customisations to the current item
262
        if (isset($custom_association)) {
263
            $item->write();
264
            foreach ($customisations as $customisation) {
265
                if ($customisation instanceof LineItemCustomisation) {
266
                    if (!$customisation->exists()) {
267
                        $customisation->write();
268
                    }
269
                    $item->{$custom_association}()->add($customisation);
270
                }
271
            }
272
        }
273
        
274
        // Ensure we update the item key
275
        $item->write();
276
277
        // If the current cart isn't in the DB, save it
278
        if (!$cart->exists()) {
279
            $this->save();
280
        }
281
282
        // Check if object already in the cart, update quantity
283
        // and delete new item
284
        $existing_item = $cart->Items()->find("Key", $item->Key);
285
286
        if (isset($existing_item)) {
287
            $this->updateItem(
288
                $existing_item,
289
                $existing_item->Quantity + $item->Quantity
290
            );
291
            $item->delete();
292
            $added = true;
293
        }
294
295
        // If no update was sucessfull then add item
296
        if (!$added) {
297
            // If we need to track stock, do it now
298
            if ($stock_item && ($stock_item->Stocked || $this->config()->check_stock_levels)) {
299
                if ($item->checkStockLevel($item->Quantity) < 0) {
300
                    throw new ValidationException(_t(
301
                        "ShoppingCart.NotEnoughStock",
302
                        "There are not enough '{title}' in stock",
303
                        ['title' => $stock_item->Title]
304
                    ));
305
                }
306
            }
307
308
            $item->ParentID = $cart->ID;
309
            $item->write();
310
        }
311
    
312
        $this->save();
313
314
        return $this;
315
    }
316
317
    /**
318
     * Find an existing item and update its quantity
319
     *
320
     * @param LineItem $item     the item in the cart to update
321
     * @param int      $quantity the new quantity
322
     * 
323
     * @throws ValidationException
324
     * @return self
325
     */
326
    public function updateItem($item, $quantity)
327
    {
328
        $stock_item = $item->FindStockItem();
329
330
        if (!$item instanceof LineItem) {
0 ignored issues
show
introduced by
$item is always a sub-type of SilverCommerce\OrdersAdmin\Model\LineItem.
Loading history...
331
            throw new ValidationException(_t(
332
                "ShoppingCart.WrongItemClass",
333
                "Item needs to be of class {class}",
334
                ["class" => LineItem::class]
335
            ));
336
        }
337
        
338
        if ($item->Locked) {
339
            throw new ValidationException(_t(
340
                "ShoppingCart.UnableToEditItem",
341
                "Unable to change item's quantity"
342
            ));
343
        }
344
345
        // If we need to track stock, do it now
346
        if ($stock_item && ($stock_item->Stocked || $this->config()->check_stock_levels)) {
347
            if ($item->checkStockLevel($quantity) < 0) {
348
                $item->Quantity = $stock_item->StockLevel;
349
                $item->write();
350
                throw new ValidationException(_t(
351
                    "ShoppingCart.NotEnoughStock",
352
                    "There are not enough '{title}' in stock",
353
                    ['title' => $stock_item->Title]
354
                ));
355
            }
356
        }
357
        
358
        $item->Quantity = floor($quantity);
359
        $item->write();
360
        
361
        return $this;
362
    }
363
364
    /**
365
     * Remove a LineItem from ShoppingCart
366
     *
367
     * @param LineItem $item The item to remove
368
     * 
369
     * @return self
370
     */
371
    public function removeItem($item)
372
    {
373
        if (!$item instanceof LineItem) {
0 ignored issues
show
introduced by
$item is always a sub-type of SilverCommerce\OrdersAdmin\Model\LineItem.
Loading history...
374
            throw new ValidationException(_t(
375
                "ShoppingCart.WrongItemClass",
376
                "Item needs to be of class {class}",
377
                ["class" => LineItem::class]
378
            ));
379
        }
380
381
        $item->delete();
382
        $this->save();
383
384
        return $this;
385
    }
386
387
388
    /**
389
     * Destroy current shopping cart
390
     * 
391
     * @return self
392
     */
393
    public function delete()
394
    {
395
        $cookies = $this->cookiesSupported();
396
        $cart = $this->getCurrent();
397
398
        // Only delete the cart if it has been written to the DB
399
        if ($cart->exists()) {
400
            $cart->delete();
401
        }
402
403
        if ($cookies) {
404
            Cookie::force_expiry(self::COOKIE_NAME);
405
        } else {
406
            $this->getSession()->clear(self::COOKIE_NAME);
407
        }
408
409
        return $this;
410
    }
411
412
    /**
413
     * Save the current shopping cart, by writing it to the DB and
414
     * generating a cookie/session (if user not logged in).
415
     *
416
     * @return self
417
     */
418
    public function save()
419
    {
420
        $cookies = $this->cookiesSupported();
421
        $session = $this->getSession();
422
        $member = Security::getCurrentUser();
423
        $cart = $this->getCurrent();
424
        $cart->recalculateDiscounts();
425
426
        // If the cart exists and the current user's cart doesn't
427
        // match, they have just logged in, replace their cart with
428
        // the new one.
429
        if ($cart->exists() && isset($member) && $member->getCart() != $cart) {
430
            $member->setCart($cart);
431
432
            if ($cookies) {
433
                Cookie::force_expiry(self::COOKIE_NAME);
434
            } else {
435
                $session->clear(self::COOKIE_NAME);
436
            }
437
        }
438
439
        $cart->write();
440
441
        if (!$member && $cookies) {
442
            Cookie::set(self::COOKIE_NAME, $cart->AccessKey);
443
        } elseif (!$member) {
444
            $session->set(self::COOKIE_NAME, $cart->AccessKey);
445
        }
446
447
        return $this;
448
    }
449
}
450