Completed
Push — master ( 2822e0...4e5716 )
by darryl
11s
created

Cart::getSubTotalWithoutConditions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 10
loc 10
rs 9.4285
cc 1
eloc 5
nc 1
nop 1
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
    /**
16
     * the item storage
17
     *
18
     * @var
19
     */
20
    protected $session;
21
22
    /**
23
     * the event dispatcher
24
     *
25
     * @var
26
     */
27
    protected $events;
28
29
    /**
30
     * the cart session key
31
     *
32
     * @var
33
     */
34
    protected $instanceName;
35
36
    /**
37
     * the session key use to persist cart items
38
     *
39
     * @var
40
     */
41
    protected $sessionKeyCartItems;
42
43
    /**
44
     * the session key use to persist cart conditions
45
     *
46
     * @var
47
     */
48
    protected $sessionKeyCartConditions;
49
50
    /**
51
     * Configuration to pass to ItemCollection
52
     *
53
     * @var
54
     */
55
    protected $config;
56
57
    /**
58
     * our object constructor
59
     *
60
     * @param $session
61
     * @param $events
62
     * @param $instanceName
63
     * @param $session_key
64
     * @param $config
65
     */
66
    public function __construct($session, $events, $instanceName, $session_key, $config)
67
    {
68
        $this->events = $events;
69
        $this->session = $session;
70
        $this->instanceName = $instanceName;
71
        $this->sessionKeyCartItems = $session_key . '_cart_items';
72
        $this->sessionKeyCartConditions = $session_key . '_cart_conditions';
73
        $this->fireEvent('created');
74
        $this->config = $config;
75
    }
76
77
    /**
78
     * get instance name of the cart
79
     *
80
     * @return string
81
     */
82
    public function getInstanceName()
83
    {
84
        return $this->instanceName;
85
    }
86
87
    /**
88
     * get an item on a cart by item ID
89
     *
90
     * @param $itemId
91
     * @return mixed
92
     */
93
    public function get($itemId)
94
    {
95
        return $this->getContent()->get($itemId);
96
    }
97
98
    /**
99
     * check if an item exists by item ID
100
     *
101
     * @param $itemId
102
     * @return bool
103
     */
104
    public function has($itemId)
105
    {
106
        return $this->getContent()->has($itemId);
107
    }
108
109
    /**
110
     * add item to the cart, it can be an array or multi dimensional array
111
     *
112
     * @param string|array $id
113
     * @param string $name
114
     * @param float $price
115
     * @param int $quantity
116
     * @param array $attributes
117
     * @param CartCondition|array $conditions
118
     * @return $this
119
     * @throws InvalidItemException
120
     */
121
    public function add($id, $name = null, $price = null, $quantity = null, $attributes = array(), $conditions = array())
122
    {
123
        // if the first argument is an array,
124
        // we will need to call add again
125
        if (is_array($id)) {
126
            // the first argument is an array, now we will need to check if it is a multi dimensional
127
            // array, if so, we will iterate through each item and call add again
128
            if (Helpers::isMultiArray($id)) {
129
                foreach ($id as $item) {
130
                    $this->add(
131
                        $item['id'],
132
                        $item['name'],
133
                        $item['price'],
134
                        $item['quantity'],
135
                        Helpers::issetAndHasValueOrAssignDefault($item['attributes'], array()),
136
                        Helpers::issetAndHasValueOrAssignDefault($item['conditions'], array())
137
                    );
138
                }
139
            } else {
140
                $this->add(
141
                    $id['id'],
142
                    $id['name'],
143
                    $id['price'],
144
                    $id['quantity'],
145
                    Helpers::issetAndHasValueOrAssignDefault($id['attributes'], array()),
146
                    Helpers::issetAndHasValueOrAssignDefault($id['conditions'], array())
147
                );
148
            }
149
150
            return $this;
151
        }
152
153
        // validate data
154
        $item = $this->validate(array(
155
            'id' => $id,
156
            'name' => $name,
157
            'price' => Helpers::normalizePrice($price),
158
            'quantity' => $quantity,
159
            'attributes' => new ItemAttributeCollection($attributes),
160
            'conditions' => $conditions,
161
        ));
162
163
        // get the cart
164
        $cart = $this->getContent();
165
166
        // if the item is already in the cart we will just update it
167
        if ($cart->has($id)) {
168
169
            $this->update($id, $item);
170
        } else {
171
172
            $this->addRow($id, $item);
173
174
        }
175
176
        return $this;
177
    }
178
179
    /**
180
     * update a cart
181
     *
182
     * @param $id
183
     * @param $data
184
     *
185
     * the $data will be an associative array, you don't need to pass all the data, only the key value
186
     * of the item you want to update on it
187
     * @return bool
188
     */
189
    public function update($id, $data)
190
    {
191
        if($this->fireEvent('updating', $data) === false) {
192
            return false;
193
        }
194
195
        $cart = $this->getContent();
196
197
        $item = $cart->pull($id);
198
199
        foreach ($data as $key => $value) {
200
            // if the key is currently "quantity" we will need to check if an arithmetic
201
            // symbol is present so we can decide if the update of quantity is being added
202
            // or being reduced.
203
            if ($key == 'quantity') {
204
                // we will check if quantity value provided is array,
205
                // if it is, we will need to check if a key "relative" is set
206
                // and we will evaluate its value if true or false,
207
                // this tells us how to treat the quantity value if it should be updated
208
                // relatively to its current quantity value or just totally replace the value
209
                if (is_array($value)) {
210
                    if (isset($value['relative'])) {
211
                        if ((bool)$value['relative']) {
212
                            $item = $this->updateQuantityRelative($item, $key, $value['value']);
213
                        } else {
214
                            $item = $this->updateQuantityNotRelative($item, $key, $value['value']);
215
                        }
216
                    }
217
                } else {
218
                    $item = $this->updateQuantityRelative($item, $key, $value);
219
                }
220
            } elseif ($key == 'attributes') {
221
                $item[$key] = new ItemAttributeCollection($value);
222
            } else {
223
                $item[$key] = $value;
224
            }
225
        }
226
227
        $cart->put($id, $item);
228
229
        $this->save($cart);
230
231
        $this->fireEvent('updated', $item);
232
        return true;
233
    }
234
235
    /**
236
     * add condition on an existing item on the cart
237
     *
238
     * @param int|string $productId
239
     * @param CartCondition $itemCondition
240
     * @return $this
241
     */
242
    public function addItemCondition($productId, $itemCondition)
243
    {
244
        if ($product = $this->get($productId)) {
245
            $conditionInstance = "\\Darryldecode\\Cart\\CartCondition";
246
247
            if ($itemCondition instanceof $conditionInstance) {
248
                // we need to copy first to a temporary variable to hold the conditions
249
                // to avoid hitting this error "Indirect modification of overloaded element of Darryldecode\Cart\ItemCollection has no effect"
250
                // this is due to laravel Collection instance that implements Array Access
251
                // // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect
252
                $itemConditionTempHolder = $product['conditions'];
253
254
                if (is_array($itemConditionTempHolder)) {
255
                    array_push($itemConditionTempHolder, $itemCondition);
256
                } else {
257
                    $itemConditionTempHolder = $itemCondition;
258
                }
259
260
                $this->update($productId, array(
261
                    'conditions' => $itemConditionTempHolder // the newly updated conditions
262
                ));
263
            }
264
        }
265
266
        return $this;
267
    }
268
269
    /**
270
     * removes an item on cart by item ID
271
     *
272
     * @param $id
273
     * @return bool
274
     */
275
    public function remove($id)
276
    {
277
        $cart = $this->getContent();
278
279
        if($this->fireEvent('removing', $id) === false) {
280
            return false;
281
        }
282
283
        $cart->forget($id);
284
285
        $this->save($cart);
286
287
        $this->fireEvent('removed', $id);
288
        return true;
289
    }
290
291
    /**
292
     * clear cart
293
     * @return bool
294
     */
295
    public function clear()
296
    {
297
        if($this->fireEvent('clearing') === false) {
298
            return false;
299
        }
300
301
        $this->session->put(
302
            $this->sessionKeyCartItems,
303
            array()
304
        );
305
306
        $this->fireEvent('cleared');
307
        return true;
308
    }
309
310
    /**
311
     * add a condition on the cart
312
     *
313
     * @param CartCondition|array $condition
314
     * @return $this
315
     * @throws InvalidConditionException
316
     */
317
    public function condition($condition)
318
    {
319
        if (is_array($condition)) {
320
            foreach ($condition as $c) {
321
                $this->condition($c);
322
            }
323
324
            return $this;
325
        }
326
327
        if (!$condition instanceof CartCondition) throw new InvalidConditionException('Argument 1 must be an instance of \'Darryldecode\Cart\CartCondition\'');
328
329
        $conditions = $this->getConditions();
330
331
        // Check if order has been applied
332
        if ($condition->getOrder() == 0) {
333
            $last = $conditions->last();
334
            $condition->setOrder(!is_null($last) ? $last->getOrder() + 1 : 1);
335
        }
336
337
        $conditions->put($condition->getName(), $condition);
338
339
        $conditions = $conditions->sortBy(function ($condition, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
340
            return $condition->getOrder();
341
        });
342
343
        $this->saveConditions($conditions);
344
345
        return $this;
346
    }
347
348
    /**
349
     * get conditions applied on the cart
350
     *
351
     * @return CartConditionCollection
352
     */
353
    public function getConditions()
354
    {
355
        return new CartConditionCollection($this->session->get($this->sessionKeyCartConditions));
356
    }
357
358
    /**
359
     * get condition applied on the cart by its name
360
     *
361
     * @param $conditionName
362
     * @return CartCondition
363
     */
364
    public function getCondition($conditionName)
365
    {
366
        return $this->getConditions()->get($conditionName);
367
    }
368
369
    /**
370
     * Get all the condition filtered by Type
371
     * Please Note that this will only return condition added on cart bases, not those conditions added
372
     * specifically on an per item bases
373
     *
374
     * @param $type
375
     * @return CartConditionCollection
376
     */
377
    public function getConditionsByType($type)
378
    {
379
        return $this->getConditions()->filter(function (CartCondition $condition) use ($type) {
380
            return $condition->getType() == $type;
381
        });
382
    }
383
384
385
    /**
386
     * Remove all the condition with the $type specified
387
     * Please Note that this will only remove condition added on cart bases, not those conditions added
388
     * specifically on an per item bases
389
     *
390
     * @param $type
391
     * @return $this
392
     */
393
    public function removeConditionsByType($type)
394
    {
395
        $this->getConditionsByType($type)->each(function ($condition) {
396
            $this->removeCartCondition($condition->getName());
397
        });
398
    }
399
400
401
    /**
402
     * removes a condition on a cart by condition name,
403
     * this can only remove conditions that are added on cart bases not conditions that are added on an item/product.
404
     * If you wish to remove a condition that has been added for a specific item/product, you may
405
     * use the removeItemCondition(itemId, conditionName) method instead.
406
     *
407
     * @param $conditionName
408
     * @return void
409
     */
410
    public function removeCartCondition($conditionName)
411
    {
412
        $conditions = $this->getConditions();
413
414
        $conditions->pull($conditionName);
415
416
        $this->saveConditions($conditions);
417
    }
418
419
    /**
420
     * remove a condition that has been applied on an item that is already on the cart
421
     *
422
     * @param $itemId
423
     * @param $conditionName
424
     * @return bool
425
     */
426
    public function removeItemCondition($itemId, $conditionName)
427
    {
428
        if (!$item = $this->getContent()->get($itemId)) {
429
            return false;
430
        }
431
432
        if ($this->itemHasConditions($item)) {
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
                foreach ($tempConditionsHolder as $k => $condition) {
447
                    if ($condition->getName() == $conditionName) {
448
                        unset($tempConditionsHolder[$k]);
449
                    }
450
                }
451
452
                $item['conditions'] = $tempConditionsHolder;
453
            }
454
455
            // if the item condition is not an array, we will check if it is
456
            // an instance of a Condition, if so, we will check if the name matches
457
            // on the given condition name the user wants to remove, if so,
458
            // lets just make $item['conditions'] an empty array as there's just 1 condition on it anyway
459
            else {
460
                $conditionInstance = "Darryldecode\\Cart\\CartCondition";
461
462
                if ($item['conditions'] instanceof $conditionInstance) {
463
                    if ($tempConditionsHolder->getName() == $conditionName) {
464
                        $item['conditions'] = array();
465
                    }
466
                }
467
            }
468
        }
469
470
        $this->update($itemId, array(
471
            'conditions' => $item['conditions']
472
        ));
473
474
        return true;
475
    }
476
477
    /**
478
     * remove all conditions that has been applied on an item that is already on the cart
479
     *
480
     * @param $itemId
481
     * @return bool
482
     */
483
    public function clearItemConditions($itemId)
484
    {
485
        if (!$item = $this->getContent()->get($itemId)) {
486
            return false;
487
        }
488
489
        $this->update($itemId, array(
490
            'conditions' => array()
491
        ));
492
493
        return true;
494
    }
495
496
    /**
497
     * clears all conditions on a cart,
498
     * this does not remove conditions that has been added specifically to an item/product.
499
     * If you wish to remove a specific condition to a product, you may use the method: removeItemCondition($itemId, $conditionName)
500
     *
501
     * @return void
502
     */
503
    public function clearCartConditions()
504
    {
505
        $this->session->put(
506
            $this->sessionKeyCartConditions,
507
            array()
508
        );
509
    }
510
511
    /**
512
     * get cart sub total without conditions
513
     * @param bool $formatted
514
     * @return float
515
     */
516 View Code Duplication
    public function getSubTotalWithoutConditions($formatted = true)
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...
517
    {
518
        $cart = $this->getContent();
519
520
        $sum = $cart->sum(function ($item) {
521
            return $item->getPriceSum();
522
        });
523
524
        return Helpers::formatValue(floatval($sum), $formatted, $this->config);
525
    }    
526
    
527
    /**
528
     * get cart sub total
529
     * @param bool $formatted
530
     * @return float
531
     */
532 View Code Duplication
    public function getSubTotal($formatted = true)
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...
533
    {
534
        $cart = $this->getContent();
535
536
        $sum = $cart->sum(function ($item) {
537
            return $item->getPriceSumWithConditions(false);
538
        });
539
540
        return Helpers::formatValue(floatval($sum), $formatted, $this->config);
541
    }
542
543
    /**
544
     * the new total in which conditions are already applied
545
     *
546
     * @return float
547
     */
548
    public function getTotal()
549
    {
550
        $subTotal = $this->getSubTotal(false);
551
552
        $newTotal = 0.00;
553
554
        $process = 0;
555
556
        $conditions = $this
557
            ->getConditions()
558
            ->filter(function ($cond) {
559
                return $cond->getTarget() === 'subtotal';
560
            });
561
562
        // if no conditions were added, just return the sub total
563
        if (!$conditions->count()) {
564
            return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config);
565
        }
566
567
        $conditions
568
            ->each(function ($cond) use ($subTotal, &$newTotal, &$process) {
569
                $toBeCalculated = ($process > 0) ? $newTotal : $subTotal;
570
571
                $newTotal = $cond->applyCondition($toBeCalculated);
572
573
                $process++;
574
575
            });
576
577
        return Helpers::formatValue($newTotal, $this->config['format_numbers'], $this->config);
578
    }
579
580
    /**
581
     * get total quantity of items in the cart
582
     *
583
     * @return int
584
     */
585
    public function getTotalQuantity()
586
    {
587
        $items = $this->getContent();
588
589
        if ($items->isEmpty()) return 0;
590
591
        $count = $items->sum(function ($item) {
592
            return $item['quantity'];
593
        });
594
595
        return $count;
596
    }
597
598
    /**
599
     * get the cart
600
     *
601
     * @return CartCollection
602
     */
603
    public function getContent()
604
    {
605
        return (new CartCollection($this->session->get($this->sessionKeyCartItems)));
606
    }
607
608
    /**
609
     * check if cart is empty
610
     *
611
     * @return bool
612
     */
613
    public function isEmpty()
614
    {
615
        $cart = new CartCollection($this->session->get($this->sessionKeyCartItems));
616
617
        return $cart->isEmpty();
618
    }
619
620
    /**
621
     * validate Item data
622
     *
623
     * @param $item
624
     * @return array $item;
625
     * @throws InvalidItemException
626
     */
627 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...
628
    {
629
        $rules = array(
630
            'id' => 'required',
631
            'price' => 'required|numeric',
632
            'quantity' => 'required|numeric|min:1',
633
            'name' => 'required',
634
        );
635
636
        $validator = CartItemValidator::make($item, $rules);
637
638
        if ($validator->fails()) {
639
            throw new InvalidItemException($validator->messages()->first());
640
        }
641
642
        return $item;
643
    }
644
645
    /**
646
     * add row to cart collection
647
     *
648
     * @param $id
649
     * @param $item
650
     * @return bool
651
     */
652
    protected function addRow($id, $item)
653
    {
654
        if($this->fireEvent('adding', $item) === false) {
655
            return false;
656
        }
657
658
        $cart = $this->getContent();
659
660
        $cart->put($id, new ItemCollection($item, $this->config));
661
662
        $this->save($cart);
663
664
        $this->fireEvent('added', $item);
665
666
        return true;
667
    }
668
669
    /**
670
     * save the cart
671
     *
672
     * @param $cart CartCollection
673
     */
674
    protected function save($cart)
675
    {
676
        $this->session->put($this->sessionKeyCartItems, $cart);
677
    }
678
679
    /**
680
     * save the cart conditions
681
     *
682
     * @param $conditions
683
     */
684
    protected function saveConditions($conditions)
685
    {
686
        $this->session->put($this->sessionKeyCartConditions, $conditions);
687
    }
688
689
    /**
690
     * check if an item has condition
691
     *
692
     * @param $item
693
     * @return bool
694
     */
695
    protected function itemHasConditions($item)
696
    {
697
        if (!isset($item['conditions'])) return false;
698
699
        if (is_array($item['conditions'])) {
700
            return count($item['conditions']) > 0;
701
        }
702
703
        $conditionInstance = "Darryldecode\\Cart\\CartCondition";
704
705
        if ($item['conditions'] instanceof $conditionInstance) return true;
706
707
        return false;
708
    }
709
710
    /**
711
     * update a cart item quantity relative to its current quantity
712
     *
713
     * @param $item
714
     * @param $key
715
     * @param $value
716
     * @return mixed
717
     */
718
    protected function updateQuantityRelative($item, $key, $value)
719
    {
720
        if (preg_match('/\-/', $value) == 1) {
721
            $value = (int)str_replace('-', '', $value);
722
723
            // we will not allowed to reduced quantity to 0, so if the given value
724
            // would result to item quantity of 0, we will not do it.
725
            if (($item[$key] - $value) > 0) {
726
                $item[$key] -= $value;
727
            }
728
        } elseif (preg_match('/\+/', $value) == 1) {
729
            $item[$key] += (int)str_replace('+', '', $value);
730
        } else {
731
            $item[$key] += (int)$value;
732
        }
733
734
        return $item;
735
    }
736
737
    /**
738
     * update cart item quantity not relative to its current quantity value
739
     *
740
     * @param $item
741
     * @param $key
742
     * @param $value
743
     * @return mixed
744
     */
745
    protected function updateQuantityNotRelative($item, $key, $value)
746
    {
747
        $item[$key] = (int)$value;
748
749
        return $item;
750
    }
751
752
    /**
753
     * Setter for decimals. Change value on demand.
754
     * @param $decimals
755
     */
756
    public function setDecimals($decimals)
757
    {
758
        $this->decimals = $decimals;
0 ignored issues
show
Bug introduced by
The property decimals does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
759
    }
760
761
    /**
762
     * Setter for decimals point. Change value on demand.
763
     * @param $dec_point
764
     */
765
    public function setDecPoint($dec_point)
766
    {
767
        $this->dec_point = $dec_point;
0 ignored issues
show
Bug introduced by
The property dec_point does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
768
    }
769
770
    public function setThousandsSep($thousands_sep)
771
    {
772
        $this->thousands_sep = $thousands_sep;
0 ignored issues
show
Bug introduced by
The property thousands_sep does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
773
    }
774
775
    /**
776
     * @param $name
777
     * @param $value
778
     * @return mixed
779
     */
780
    protected function fireEvent($name, $value = [])
781
    {
782
        return $this->events->fire($this->getInstanceName() . '.' . $name, array_values([$value, $this]));
783
    }
784
}
785