Completed
Push — master ( c6ee56...bd7a62 )
by darryl
06:18
created

Cart   D

Complexity

Total Complexity 65

Size/Duplication

Total Lines 732
Duplicated Lines 5.74 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 19
Bugs 5 Features 10
Metric Value
wmc 65
c 19
b 5
f 10
lcom 1
cbo 8
dl 42
loc 732
rs 4

30 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getInstanceName() 0 4 1
A get() 0 4 1
A has() 0 4 1
B add() 0 65 5
B update() 0 55 7
B addItemCondition() 0 31 4
A remove() 12 12 1
A clear() 0 11 1
B condition() 0 22 4
A getConditions() 0 4 1
A getCondition() 0 4 1
A getConditionsByType() 0 7 1
A removeConditionsByType() 0 7 1
A removeCartCondition() 0 8 1
B removeItemCondition() 0 58 8
A clearItemConditions() 0 13 2
A clearCartConditions() 0 7 1
A getSubTotal() 0 11 1
B getTotal() 0 27 4
A getTotalQuantity() 0 13 2
A getContent() 0 4 1
A isEmpty() 0 6 1
A validate() 18 18 2
A addRow() 12 12 1
A save() 0 4 1
A saveConditions() 0 4 1
A itemHasConditions() 0 15 4
B updateQuantityRelative() 0 24 4
A updateQuantityNotRelative() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Cart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cart, and based on these observations, apply Extract Interface, too.

1
<?php namespace Darryldecode\Cart;
2
3
use Darryldecode\Cart\Exceptions\InvalidConditionException;
4
use Darryldecode\Cart\Exceptions\InvalidItemException;
5
use Darryldecode\Cart\Helpers\Helpers;
6
use Darryldecode\Cart\Validators\CartItemValidator;
7
8
/**
9
 * Class Cart
10
 * @package Darryldecode\Cart
11
 */
12
class Cart {
13
14
    /**
15
     * the item storage
16
     *
17
     * @var
18
     */
19
    protected $session;
20
21
    /**
22
     * the event dispatcher
23
     *
24
     * @var
25
     */
26
    protected $events;
27
28
    /**
29
     * the cart session key
30
     *
31
     * @var
32
     */
33
    protected $instanceName;
34
35
    /**
36
     * the session key use to persist cart items
37
     *
38
     * @var
39
     */
40
    protected $sessionKeyCartItems;
41
42
    /**
43
     * the session key use to persist cart conditions
44
     *
45
     * @var
46
     */
47
    protected $sessionKeyCartConditions;
48
49
    /**
50
     * our object constructor
51
     *
52
     * @param $session
53
     * @param $events
54
     * @param $instanceName
55
     * @param $session_key
56
     */
57
    public function __construct($session, $events, $instanceName, $session_key)
58
    {
59
        $this->events = $events;
60
        $this->session = $session;
61
        $this->instanceName = $instanceName;
62
        $this->sessionKeyCartItems = $session_key.'_cart_items';
63
        $this->sessionKeyCartConditions = $session_key.'_cart_conditions';
64
        $this->events->fire($this->getInstanceName().'.created', array($this));
65
    }
66
67
    /**
68
     * get instance name of the cart
69
     *
70
     * @return string
71
     */
72
    public function getInstanceName()
73
    {
74
        return $this->instanceName;
75
    }
76
77
    /**
78
     * get an item on a cart by item ID
79
     *
80
     * @param $itemId
81
     * @return mixed
82
     */
83
    public function get($itemId)
84
    {
85
        return $this->getContent()->get($itemId);
86
    }
87
88
    /**
89
     * check if an item exists by item ID
90
     *
91
     * @param $itemId
92
     * @return bool
93
     */
94
    public function has($itemId)
95
    {
96
        return $this->getContent()->has($itemId);
97
    }
98
99
    /**
100
     * add item to the cart, it can be an array or multi dimensional array
101
     *
102
     * @param string|array $id
103
     * @param string $name
104
     * @param float $price
105
     * @param int $quantity
106
     * @param array $attributes
107
     * @param CartCondition|array $conditions
108
     * @return $this
109
     * @throws InvalidItemException
110
     */
111
    public function add($id, $name = null, $price = null, $quantity = null, $attributes = array(), $conditions = array())
112
    {
113
        // if the first argument is an array,
114
        // we will need to call add again
115
        if( is_array($id) )
116
        {
117
            // the first argument is an array, now we will need to check if it is a multi dimensional
118
            // array, if so, we will iterate through each item and call add again
119
            if( Helpers::isMultiArray($id) )
120
            {
121
                foreach($id as $item)
122
                {
123
                    $this->add(
124
                        $item['id'],
125
                        $item['name'],
126
                        $item['price'],
127
                        $item['quantity'],
128
                        Helpers::issetAndHasValueOrAssignDefault($item['attributes'], array()),
129
                        Helpers::issetAndHasValueOrAssignDefault($item['conditions'], array())
130
                    );
131
                }
132
            }
133
            else
134
            {
135
                $this->add(
136
                    $id['id'],
137
                    $id['name'],
138
                    $id['price'],
139
                    $id['quantity'],
140
                    Helpers::issetAndHasValueOrAssignDefault($id['attributes'], array()),
141
                    Helpers::issetAndHasValueOrAssignDefault($id['conditions'], array())
142
                );
143
            }
144
145
            return $this;
146
        }
147
148
        // validate data
149
        $item = $this->validate(array(
150
            'id' => $id,
151
            'name' => $name,
152
            'price' => Helpers::normalizePrice($price),
153
            'quantity' => $quantity,
154
            'attributes' => new ItemAttributeCollection($attributes),
155
            'conditions' => $conditions,
156
        ));
157
158
        // get the cart
159
        $cart = $this->getContent();
160
161
        // if the item is already in the cart we will just update it
162
        if( $cart->has($id) )
163
        {
164
165
            $this->update($id, $item);
166
        }
167
        else
168
        {
169
170
            $this->addRow($id, $item);
171
172
        }
173
174
        return $this;
175
    }
176
177
    /**
178
     * update a cart
179
     *
180
     * @param $id
181
     * @param $data
182
     *
183
     * the $data will be an associative array, you don't need to pass all the data, only the key value
184
     * of the item you want to update on it
185
     */
186
    public function update($id, $data)
187
    {
188
        $this->events->fire($this->getInstanceName().'.updating', array($data, $this));
189
190
        $cart = $this->getContent();
191
192
        $item = $cart->pull($id);
193
194
        foreach($data as $key => $value)
195
        {
196
            // if the key is currently "quantity" we will need to check if an arithmetic
197
            // symbol is present so we can decide if the update of quantity is being added
198
            // or being reduced.
199
            if( $key == 'quantity' )
200
            {
201
                // we will check if quantity value provided is array,
202
                // if it is, we will need to check if a key "relative" is set
203
                // and we will evaluate its value if true or false,
204
                // this tells us how to treat the quantity value if it should be updated
205
                // relatively to its current quantity value or just totally replace the value
206
                if( is_array($value) )
207
                {
208
                    if( isset($value['relative']) )
209
                    {
210
                        if( (bool) $value['relative'] )
211
                        {
212
                            $item = $this->updateQuantityRelative($item, $key, $value['value']);
213
                        }
214
                        else
215
                        {
216
                            $item = $this->updateQuantityNotRelative($item, $key, $value['value']);
217
                        }
218
                    }
219
                }
220
                else
221
                {
222
                    $item = $this->updateQuantityRelative($item, $key, $value);
223
                }
224
            }
225
            elseif( $key == 'attributes' )
226
            {
227
                $item[$key] = new ItemAttributeCollection($value);
228
            }
229
            else
230
            {
231
                $item[$key] = $value;
232
            }
233
        }
234
235
        $cart->put($id, $item);
236
237
        $this->save($cart);
238
239
        $this->events->fire($this->getInstanceName().'.updated', array($item, $this));
240
    }
241
242
    /**
243
     * add condition on an existing item on the cart
244
     *
245
     * @param int|string $productId
246
     * @param CartCondition $itemCondition
247
     * @return $this
248
     */
249
    public function addItemCondition($productId, $itemCondition)
250
    {
251
        if( $product = $this->get($productId) )
252
        {
253
            $conditionInstance = "\\Darryldecode\\Cart\\CartCondition";
254
255
            if( $itemCondition instanceof $conditionInstance )
256
            {
257
                // we need to copy first to a temporary variable to hold the conditions
258
                // to avoid hitting this error "Indirect modification of overloaded element of Darryldecode\Cart\ItemCollection has no effect"
259
                // this is due to laravel Collection instance that implements Array Access
260
                // // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect
261
                $itemConditionTempHolder = $product['conditions'];
262
263
                if( is_array($itemConditionTempHolder) )
264
                {
265
                    array_push($itemConditionTempHolder, $itemCondition);
266
                }
267
                else
268
                {
269
                    $itemConditionTempHolder = $itemCondition;
270
                }
271
272
                $this->update($productId, array(
273
                    'conditions' => $itemConditionTempHolder // the newly updated conditions
274
                ));
275
            }
276
        }
277
278
        return $this;
279
    }
280
281
    /**
282
     * removes an item on cart by item ID
283
     *
284
     * @param $id
285
     */
286 View Code Duplication
    public function remove($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
287
    {
288
        $cart = $this->getContent();
289
290
        $this->events->fire($this->getInstanceName().'.removing', array($id, $this));
291
292
        $cart->forget($id);
293
294
        $this->save($cart);
295
296
        $this->events->fire($this->getInstanceName().'.removed', array($id, $this));
297
    }
298
299
    /**
300
     * clear cart
301
     */
302
    public function clear()
303
    {
304
        $this->events->fire($this->getInstanceName().'.clearing', array($this));
305
306
        $this->session->put(
307
            $this->sessionKeyCartItems,
308
            array()
309
        );
310
311
        $this->events->fire($this->getInstanceName().'.cleared', array($this));
312
    }
313
314
    /**
315
     * add a condition on the cart
316
     *
317
     * @param CartCondition|array $condition
318
     * @return $this
319
     * @throws InvalidConditionException
320
     */
321
    public function condition($condition)
322
    {
323
        if( is_array($condition) )
324
        {
325
            foreach($condition as $c)
326
            {
327
                $this->condition($c);
328
            }
329
330
            return $this;
331
        }
332
333
        if( ! $condition instanceof CartCondition ) throw new InvalidConditionException('Argument 1 must be an instance of \'Darryldecode\Cart\CartCondition\'');
334
335
        $conditions = $this->getConditions();
336
337
        $conditions->put($condition->getName(), $condition);
338
339
        $this->saveConditions($conditions);
340
341
        return $this;
342
    }
343
344
    /**
345
     * get conditions applied on the cart
346
     *
347
     * @return CartConditionCollection
348
     */
349
    public function getConditions()
350
    {
351
        return new CartConditionCollection($this->session->get($this->sessionKeyCartConditions));
352
    }
353
354
    /**
355
     * get condition applied on the cart by its name
356
     *
357
     * @param $conditionName
358
     * @return CartCondition
359
     */
360
    public function getCondition($conditionName)
361
    {
362
        return $this->getConditions()->get($conditionName);
363
    }
364
365
    /**
366
    * Get all the condition filtered by Type
367
    * Please Note that this will only return condition added on cart bases, not those conditions added
368
    * specifically on an per item bases
369
    *
370
    * @param $type
371
    * @return CartConditionCollection
372
    */
373
    public function getConditionsByType($type)
374
    {
375
        return $this->getConditions()->filter(function(CartCondition $condition) use ($type)
376
        {
377
            return $condition->getType() == $type;
378
        });
379
    }
380
381
382
    /**
383
     * Remove all the condition with the $type specified
384
     * Please Note that this will only remove condition added on cart bases, not those conditions added
385
     * specifically on an per item bases
386
     *
387
     * @param $type
388
     * @return $this
389
     */
390
    public function removeConditionsByType($type)
391
    {
392
        $this->getConditionsByType($type)->each(function($condition)
393
        {
394
            $this->removeCartCondition($condition->getName());
395
        });
396
    }
397
398
399
    /**
400
     * removes a condition on a cart by condition name,
401
     * this can only remove conditions that are added on cart bases not conditions that are added on an item/product.
402
     * If you wish to remove a condition that has been added for a specific item/product, you may
403
     * use the removeItemCondition(itemId, conditionName) method instead.
404
     *
405
     * @param $conditionName
406
     * @return void
407
     */
408
    public function removeCartCondition($conditionName)
409
    {
410
        $conditions = $this->getConditions();
411
412
        $conditions->pull($conditionName);
413
414
        $this->saveConditions($conditions);
415
    }
416
417
    /**
418
     * remove a condition that has been applied on an item that is already on the cart
419
     *
420
     * @param $itemId
421
     * @param $conditionName
422
     * @return bool
423
     */
424
    public function removeItemCondition($itemId, $conditionName)
425
    {
426
        if( ! $item = $this->getContent()->get($itemId) )
427
        {
428
            return false;
429
        }
430
431
        if( $this->itemHasConditions($item) )
432
        {
433
            // NOTE:
434
            // we do it this way, we get first conditions and store
435
            // it in a temp variable $originalConditions, then we will modify the array there
436
            // and after modification we will store it again on $item['conditions']
437
            // This is because of ArrayAccess implementation
438
            // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect
439
440
            $tempConditionsHolder = $item['conditions'];
441
442
            // if the item's conditions is in array format
443
            // we will iterate through all of it and check if the name matches
444
            // to the given name the user wants to remove, if so, remove it
445
            if( is_array($tempConditionsHolder) )
446
            {
447
                foreach($tempConditionsHolder as $k => $condition)
448
                {
449
                    if( $condition->getName() == $conditionName )
450
                    {
451
                        unset($tempConditionsHolder[$k]);
452
                    }
453
                }
454
455
                $item['conditions'] = $tempConditionsHolder;
456
            }
457
458
            // if the item condition is not an array, we will check if it is
459
            // an instance of a Condition, if so, we will check if the name matches
460
            // on the given condition name the user wants to remove, if so,
461
            // lets just make $item['conditions'] an empty array as there's just 1 condition on it anyway
462
            else
463
            {
464
                $conditionInstance = "Darryldecode\\Cart\\CartCondition";
465
466
                if ($item['conditions'] instanceof $conditionInstance)
467
                {
468
                    if ($tempConditionsHolder->getName() == $conditionName)
469
                    {
470
                        $item['conditions'] = array();
471
                    }
472
                }
473
            }
474
        }
475
476
        $this->update($itemId, array(
477
            'conditions' => $item['conditions']
478
        ));
479
480
        return true;
481
    }
482
483
    /**
484
     * remove all conditions that has been applied on an item that is already on the cart
485
     *
486
     * @param $itemId
487
     * @return bool
488
     */
489
    public function clearItemConditions($itemId)
490
    {
491
        if( ! $item = $this->getContent()->get($itemId) )
492
        {
493
            return false;
494
        }
495
496
        $this->update($itemId, array(
497
            'conditions' => array()
498
        ));
499
500
        return true;
501
    }
502
503
    /**
504
     * clears all conditions on a cart,
505
     * this does not remove conditions that has been added specifically to an item/product.
506
     * If you wish to remove a specific condition to a product, you may use the method: removeItemCondition($itemId, $conditionName)
507
     *
508
     * @return void
509
     */
510
    public function clearCartConditions()
511
    {
512
        $this->session->put(
513
            $this->sessionKeyCartConditions,
514
            array()
515
        );
516
    }
517
518
    /**
519
     * get cart sub total
520
     *
521
     * @return float
522
     */
523
    public function getSubTotal()
524
    {
525
        $cart = $this->getContent();
526
527
        $sum = $cart->sum(function($item)
528
        {
529
            return $item->getPriceSumWithConditions();
530
        });
531
532
        return floatval($sum);
533
    }
534
535
    /**
536
     * the new total in which conditions are already applied
537
     *
538
     * @return float
539
     */
540
    public function getTotal()
541
    {
542
        $subTotal = $this->getSubTotal();
543
544
        $newTotal = 0.00;
545
546
        $process = 0;
547
548
        $conditions = $this->getConditions();
549
550
        // if no conditions were added, just return the sub total
551
        if( ! $conditions->count() ) return $subTotal;
552
553
        $conditions->each(function($cond) use ($subTotal, &$newTotal, &$process)
554
        {
555
            if( $cond->getTarget() === 'subtotal' )
556
            {
557
                ( $process > 0 ) ? $toBeCalculated = $newTotal : $toBeCalculated = $subTotal;
558
559
                $newTotal = $cond->applyCondition($toBeCalculated);
560
561
                $process++;
562
            }
563
        });
564
565
        return $newTotal;
566
    }
567
568
    /**
569
     * get total quantity of items in the cart
570
     *
571
     * @return int
572
     */
573
    public function getTotalQuantity()
574
    {
575
        $items = $this->getContent();
576
577
        if( $items->isEmpty() ) return 0;
578
579
        $count = $items->sum(function($item)
580
        {
581
            return $item['quantity'];
582
        });
583
584
        return $count;
585
    }
586
587
    /**
588
     * get the cart
589
     *
590
     * @return CartCollection
591
     */
592
    public function getContent()
593
    {
594
        return (new CartCollection($this->session->get($this->sessionKeyCartItems)));
595
    }
596
597
    /**
598
     * check if cart is empty
599
     *
600
     * @return bool
601
     */
602
    public function isEmpty()
603
    {
604
        $cart = new CartCollection($this->session->get($this->sessionKeyCartItems));
605
606
        return $cart->isEmpty();
607
    }
608
609
    /**
610
     * validate Item data
611
     *
612
     * @param $item
613
     * @return array $item;
614
     * @throws InvalidItemException
615
     */
616 View Code Duplication
    protected function validate($item)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
617
    {
618
        $rules = array(
619
            'id' => 'required',
620
            'price' => 'required|numeric',
621
            'quantity' => 'required|numeric|min:1',
622
            'name' => 'required',
623
        );
624
625
        $validator = CartItemValidator::make($item, $rules);
626
627
        if( $validator->fails() )
628
        {
629
            throw new InvalidItemException($validator->messages()->first());
630
        }
631
632
        return $item;
633
    }
634
635
    /**
636
     * add row to cart collection
637
     *
638
     * @param $id
639
     * @param $item
640
     */
641 View Code Duplication
    protected function addRow($id, $item)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
642
    {
643
        $this->events->fire($this->getInstanceName().'.adding', array($item, $this));
644
645
        $cart = $this->getContent();
646
647
        $cart->put($id, new ItemCollection($item));
648
649
        $this->save($cart);
650
651
        $this->events->fire($this->getInstanceName().'.added', array($item, $this));
652
    }
653
654
    /**
655
     * save the cart
656
     *
657
     * @param $cart CartCollection
658
     */
659
    protected function save($cart)
660
    {
661
        $this->session->put($this->sessionKeyCartItems, $cart);
662
    }
663
664
    /**
665
     * save the cart conditions
666
     *
667
     * @param $conditions
668
     */
669
    protected function saveConditions($conditions)
670
    {
671
        $this->session->put($this->sessionKeyCartConditions, $conditions);
672
    }
673
674
    /**
675
     * check if an item has condition
676
     *
677
     * @param $item
678
     * @return bool
679
     */
680
    protected function itemHasConditions($item)
681
    {
682
        if( ! isset($item['conditions']) ) return false;
683
684
        if( is_array($item['conditions']) )
685
        {
686
            return count($item['conditions']) > 0;
687
        }
688
689
        $conditionInstance = "Darryldecode\\Cart\\CartCondition";
690
691
        if( $item['conditions'] instanceof $conditionInstance ) return true;
692
693
        return false;
694
    }
695
696
    /**
697
     * update a cart item quantity relative to its current quantity
698
     *
699
     * @param $item
700
     * @param $key
701
     * @param $value
702
     * @return mixed
703
     */
704
    protected function updateQuantityRelative($item, $key, $value)
705
    {
706
        if( preg_match('/\-/', $value) == 1 )
707
        {
708
            $value = (int) str_replace('-','',$value);
709
710
            // we will not allowed to reduced quantity to 0, so if the given value
711
            // would result to item quantity of 0, we will not do it.
712
            if( ($item[$key] - $value) > 0 )
713
            {
714
                $item[$key] -= $value;
715
            }
716
        }
717
        elseif( preg_match('/\+/', $value) == 1 )
718
        {
719
            $item[$key] += (int) str_replace('+','',$value);
720
        }
721
        else
722
        {
723
            $item[$key] += (int) $value;
724
        }
725
726
        return $item;
727
    }
728
729
    /**
730
     * update cart item quantity not relative to its current quantity value
731
     *
732
     * @param $item
733
     * @param $key
734
     * @param $value
735
     * @return mixed
736
     */
737
    protected function updateQuantityNotRelative($item, $key, $value)
738
    {
739
        $item[$key] = (int) $value;
740
741
        return $item;
742
    }
743
}
744