Passed
Push — 1.0 ( dd1df7...8582b7 )
by Morven
02:27
created

ShoppingCartFactory::updateItem()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

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