Completed
Pull Request — master (#112)
by darryl
07:48
created

Cart::session()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 6
nc 2
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 for the cart
38
     *
39
     * @var
40
     */
41
    protected $sessionKey;
42
43
    /**
44
     * the session key use to persist cart items
45
     *
46
     * @var
47
     */
48
    protected $sessionKeyCartItems;
49
50
    /**
51
     * the session key use to persist cart conditions
52
     *
53
     * @var
54
     */
55
    protected $sessionKeyCartConditions;
56
57
    /**
58
     * Configuration to pass to ItemCollection
59
     *
60
     * @var
61
     */
62
    protected $config;
63
64
    /**
65
     * our object constructor
66
     *
67
     * @param $session
68
     * @param $events
69
     * @param $instanceName
70
     * @param $session_key
71
     * @param $config
72
     */
73
    public function __construct($session, $events, $instanceName, $session_key, $config)
74
    {
75
        $this->events = $events;
76
        $this->session = $session;
77
        $this->instanceName = $instanceName;
78
        $this->sessionKey = $session_key;
79
        $this->sessionKeyCartItems = $this->sessionKey . '_cart_items';
80
        $this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions';
81
        $this->config = $config;
82
        $this->fireEvent('created');
83
    }
84
85
    /**
86
     * sets the session key
87
     *
88
     * @param string $sessionKey the session key or identifier
89
     * @return $this|bool
90
     * @throws \Exception
91
     */
92
    public function session($sessionKey)
93
    {
94
        if(!$sessionKey) throw new \Exception("Session key is required.");
95
96
        $this->sessionKey = $sessionKey;
97
        $this->sessionKeyCartItems = $this->sessionKey . '_cart_items';
98
        $this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions';
99
100
        return $this;
101
    }
102
103
    /**
104
     * get instance name of the cart
105
     *
106
     * @return string
107
     */
108
    public function getInstanceName()
109
    {
110
        return $this->instanceName;
111
    }
112
113
    /**
114
     * get an item on a cart by item ID
115
     *
116
     * @param $itemId
117
     * @return mixed
118
     */
119
    public function get($itemId)
120
    {
121
        return $this->getContent()->get($itemId);
122
    }
123
124
    /**
125
     * check if an item exists by item ID
126
     *
127
     * @param $itemId
128
     * @return bool
129
     */
130
    public function has($itemId)
131
    {
132
        return $this->getContent()->has($itemId);
133
    }
134
135
    /**
136
     * add item to the cart, it can be an array or multi dimensional array
137
     *
138
     * @param string|array $id
139
     * @param string $name
140
     * @param float $price
141
     * @param int $quantity
142
     * @param array $attributes
143
     * @param CartCondition|array $conditions
144
     * @return $this
145
     * @throws InvalidItemException
146
     */
147
    public function add($id, $name = null, $price = null, $quantity = null, $attributes = array(), $conditions = array())
148
    {
149
        // if the first argument is an array,
150
        // we will need to call add again
151
        if (is_array($id)) {
152
            // the first argument is an array, now we will need to check if it is a multi dimensional
153
            // array, if so, we will iterate through each item and call add again
154
            if (Helpers::isMultiArray($id)) {
155
                foreach ($id as $item) {
156
                    $this->add(
157
                        $item['id'],
158
                        $item['name'],
159
                        $item['price'],
160
                        $item['quantity'],
161
                        Helpers::issetAndHasValueOrAssignDefault($item['attributes'], array()),
162
                        Helpers::issetAndHasValueOrAssignDefault($item['conditions'], array())
163
                    );
164
                }
165
            } else {
166
                $this->add(
167
                    $id['id'],
168
                    $id['name'],
169
                    $id['price'],
170
                    $id['quantity'],
171
                    Helpers::issetAndHasValueOrAssignDefault($id['attributes'], array()),
172
                    Helpers::issetAndHasValueOrAssignDefault($id['conditions'], array())
173
                );
174
            }
175
176
            return $this;
177
        }
178
179
        // validate data
180
        $item = $this->validate(array(
181
            'id' => $id,
182
            'name' => $name,
183
            'price' => Helpers::normalizePrice($price),
184
            'quantity' => $quantity,
185
            'attributes' => new ItemAttributeCollection($attributes),
186
            'conditions' => $conditions,
187
        ));
188
189
        // get the cart
190
        $cart = $this->getContent();
191
192
        // if the item is already in the cart we will just update it
193
        if ($cart->has($id)) {
194
195
            $this->update($id, $item);
196
        } else {
197
198
            $this->addRow($id, $item);
199
200
        }
201
202
        return $this;
203
    }
204
205
    /**
206
     * update a cart
207
     *
208
     * @param $id
209
     * @param $data
210
     *
211
     * the $data will be an associative array, you don't need to pass all the data, only the key value
212
     * of the item you want to update on it
213
     * @return bool
214
     */
215
    public function update($id, $data)
216
    {
217
        if($this->fireEvent('updating', $data) === false) {
218
            return false;
219
        }
220
221
        $cart = $this->getContent();
222
223
        $item = $cart->pull($id);
224
225
        foreach ($data as $key => $value) {
226
            // if the key is currently "quantity" we will need to check if an arithmetic
227
            // symbol is present so we can decide if the update of quantity is being added
228
            // or being reduced.
229
            if ($key == 'quantity') {
230
                // we will check if quantity value provided is array,
231
                // if it is, we will need to check if a key "relative" is set
232
                // and we will evaluate its value if true or false,
233
                // this tells us how to treat the quantity value if it should be updated
234
                // relatively to its current quantity value or just totally replace the value
235
                if (is_array($value)) {
236
                    if (isset($value['relative'])) {
237
                        if ((bool)$value['relative']) {
238
                            $item = $this->updateQuantityRelative($item, $key, $value['value']);
239
                        } else {
240
                            $item = $this->updateQuantityNotRelative($item, $key, $value['value']);
241
                        }
242
                    }
243
                } else {
244
                    $item = $this->updateQuantityRelative($item, $key, $value);
245
                }
246
            } elseif ($key == 'attributes') {
247
                $item[$key] = new ItemAttributeCollection($value);
248
            } else {
249
                $item[$key] = $value;
250
            }
251
        }
252
253
        $cart->put($id, $item);
254
255
        $this->save($cart);
256
257
        $this->fireEvent('updated', $item);
258
        return true;
259
    }
260
261
    /**
262
     * add condition on an existing item on the cart
263
     *
264
     * @param int|string $productId
265
     * @param CartCondition $itemCondition
266
     * @return $this
267
     */
268
    public function addItemCondition($productId, $itemCondition)
269
    {
270
        if ($product = $this->get($productId)) {
271
            $conditionInstance = "\\Darryldecode\\Cart\\CartCondition";
272
273
            if ($itemCondition instanceof $conditionInstance) {
274
                // we need to copy first to a temporary variable to hold the conditions
275
                // to avoid hitting this error "Indirect modification of overloaded element of Darryldecode\Cart\ItemCollection has no effect"
276
                // this is due to laravel Collection instance that implements Array Access
277
                // // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect
278
                $itemConditionTempHolder = $product['conditions'];
279
280
                if (is_array($itemConditionTempHolder)) {
281
                    array_push($itemConditionTempHolder, $itemCondition);
282
                } else {
283
                    $itemConditionTempHolder = $itemCondition;
284
                }
285
286
                $this->update($productId, array(
287
                    'conditions' => $itemConditionTempHolder // the newly updated conditions
288
                ));
289
            }
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * removes an item on cart by item ID
297
     *
298
     * @param $id
299
     * @return bool
300
     */
301
    public function remove($id)
302
    {
303
        $cart = $this->getContent();
304
305
        if($this->fireEvent('removing', $id) === false) {
306
            return false;
307
        }
308
309
        $cart->forget($id);
310
311
        $this->save($cart);
312
313
        $this->fireEvent('removed', $id);
314
        return true;
315
    }
316
317
    /**
318
     * clear cart
319
     * @return bool
320
     */
321
    public function clear()
322
    {
323
        if($this->fireEvent('clearing') === false) {
324
            return false;
325
        }
326
327
        $this->session->put(
328
            $this->sessionKeyCartItems,
329
            array()
330
        );
331
332
        $this->fireEvent('cleared');
333
        return true;
334
    }
335
336
    /**
337
     * add a condition on the cart
338
     *
339
     * @param CartCondition|array $condition
340
     * @return $this
341
     * @throws InvalidConditionException
342
     */
343
    public function condition($condition)
344
    {
345
        if (is_array($condition)) {
346
            foreach ($condition as $c) {
347
                $this->condition($c);
348
            }
349
350
            return $this;
351
        }
352
353
        if (!$condition instanceof CartCondition) throw new InvalidConditionException('Argument 1 must be an instance of \'Darryldecode\Cart\CartCondition\'');
354
355
        $conditions = $this->getConditions();
356
357
        // Check if order has been applied
358
        if ($condition->getOrder() == 0) {
359
            $last = $conditions->last();
360
            $condition->setOrder(!is_null($last) ? $last->getOrder() + 1 : 1);
361
        }
362
363
        $conditions->put($condition->getName(), $condition);
364
365
        $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...
366
            return $condition->getOrder();
367
        });
368
369
        $this->saveConditions($conditions);
370
371
        return $this;
372
    }
373
374
    /**
375
     * get conditions applied on the cart
376
     *
377
     * @return CartConditionCollection
378
     */
379
    public function getConditions()
380
    {
381
        return new CartConditionCollection($this->session->get($this->sessionKeyCartConditions));
382
    }
383
384
    /**
385
     * get condition applied on the cart by its name
386
     *
387
     * @param $conditionName
388
     * @return CartCondition
389
     */
390
    public function getCondition($conditionName)
391
    {
392
        return $this->getConditions()->get($conditionName);
393
    }
394
395
    /**
396
     * Get all the condition filtered by Type
397
     * Please Note that this will only return condition added on cart bases, not those conditions added
398
     * specifically on an per item bases
399
     *
400
     * @param $type
401
     * @return CartConditionCollection
402
     */
403
    public function getConditionsByType($type)
404
    {
405
        return $this->getConditions()->filter(function (CartCondition $condition) use ($type) {
406
            return $condition->getType() == $type;
407
        });
408
    }
409
410
411
    /**
412
     * Remove all the condition with the $type specified
413
     * Please Note that this will only remove condition added on cart bases, not those conditions added
414
     * specifically on an per item bases
415
     *
416
     * @param $type
417
     * @return $this
418
     */
419
    public function removeConditionsByType($type)
420
    {
421
        $this->getConditionsByType($type)->each(function ($condition) {
422
            $this->removeCartCondition($condition->getName());
423
        });
424
    }
425
426
427
    /**
428
     * removes a condition on a cart by condition name,
429
     * this can only remove conditions that are added on cart bases not conditions that are added on an item/product.
430
     * If you wish to remove a condition that has been added for a specific item/product, you may
431
     * use the removeItemCondition(itemId, conditionName) method instead.
432
     *
433
     * @param $conditionName
434
     * @return void
435
     */
436
    public function removeCartCondition($conditionName)
437
    {
438
        $conditions = $this->getConditions();
439
440
        $conditions->pull($conditionName);
441
442
        $this->saveConditions($conditions);
443
    }
444
445
    /**
446
     * remove a condition that has been applied on an item that is already on the cart
447
     *
448
     * @param $itemId
449
     * @param $conditionName
450
     * @return bool
451
     */
452
    public function removeItemCondition($itemId, $conditionName)
453
    {
454
        if (!$item = $this->getContent()->get($itemId)) {
455
            return false;
456
        }
457
458
        if ($this->itemHasConditions($item)) {
459
            // NOTE:
460
            // we do it this way, we get first conditions and store
461
            // it in a temp variable $originalConditions, then we will modify the array there
462
            // and after modification we will store it again on $item['conditions']
463
            // This is because of ArrayAccess implementation
464
            // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect
465
466
            $tempConditionsHolder = $item['conditions'];
467
468
            // if the item's conditions is in array format
469
            // we will iterate through all of it and check if the name matches
470
            // to the given name the user wants to remove, if so, remove it
471
            if (is_array($tempConditionsHolder)) {
472
                foreach ($tempConditionsHolder as $k => $condition) {
473
                    if ($condition->getName() == $conditionName) {
474
                        unset($tempConditionsHolder[$k]);
475
                    }
476
                }
477
478
                $item['conditions'] = $tempConditionsHolder;
479
            }
480
481
            // if the item condition is not an array, we will check if it is
482
            // an instance of a Condition, if so, we will check if the name matches
483
            // on the given condition name the user wants to remove, if so,
484
            // lets just make $item['conditions'] an empty array as there's just 1 condition on it anyway
485
            else {
486
                $conditionInstance = "Darryldecode\\Cart\\CartCondition";
487
488
                if ($item['conditions'] instanceof $conditionInstance) {
489
                    if ($tempConditionsHolder->getName() == $conditionName) {
490
                        $item['conditions'] = array();
491
                    }
492
                }
493
            }
494
        }
495
496
        $this->update($itemId, array(
497
            'conditions' => $item['conditions']
498
        ));
499
500
        return true;
501
    }
502
503
    /**
504
     * remove all conditions that has been applied on an item that is already on the cart
505
     *
506
     * @param $itemId
507
     * @return bool
508
     */
509
    public function clearItemConditions($itemId)
510
    {
511
        if (!$item = $this->getContent()->get($itemId)) {
512
            return false;
513
        }
514
515
        $this->update($itemId, array(
516
            'conditions' => array()
517
        ));
518
519
        return true;
520
    }
521
522
    /**
523
     * clears all conditions on a cart,
524
     * this does not remove conditions that has been added specifically to an item/product.
525
     * If you wish to remove a specific condition to a product, you may use the method: removeItemCondition($itemId, $conditionName)
526
     *
527
     * @return void
528
     */
529
    public function clearCartConditions()
530
    {
531
        $this->session->put(
532
            $this->sessionKeyCartConditions,
533
            array()
534
        );
535
    }
536
537
    /**
538
     * get cart sub total without conditions
539
     * @param bool $formatted
540
     * @return float
541
     */
542 View Code Duplication
    public function getSubTotalWithoutConditions($formatted = true)
543
    {
544
        $cart = $this->getContent();
545
546
        $sum = $cart->sum(function ($item) {
547
            return $item->getPriceSum();
548
        });
549
550
        return Helpers::formatValue(floatval($sum), $formatted, $this->config);
551
    }    
552
    
553
    /**
554
     * get cart sub total
555
     * @param bool $formatted
556
     * @return float
557
     */
558 View Code Duplication
    public function getSubTotal($formatted = true)
559
    {
560
        $cart = $this->getContent();
561
562
        $sum = $cart->sum(function ($item) {
563
            return $item->getPriceSumWithConditions(false);
564
        });
565
566
        return Helpers::formatValue(floatval($sum), $formatted, $this->config);
567
    }
568
569
    /**
570
     * the new total in which conditions are already applied
571
     *
572
     * @return float
573
     */
574
    public function getTotal()
575
    {
576
        $subTotal = $this->getSubTotal(false);
577
578
        $newTotal = 0.00;
579
580
        $process = 0;
581
582
        $conditions = $this
583
            ->getConditions()
584
            ->filter(function ($cond) {
585
                return $cond->getTarget() === 'subtotal';
586
            });
587
588
        // if no conditions were added, just return the sub total
589
        if (!$conditions->count()) {
590
            return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config);
591
        }
592
593
        $conditions
594
            ->each(function ($cond) use ($subTotal, &$newTotal, &$process) {
595
                $toBeCalculated = ($process > 0) ? $newTotal : $subTotal;
596
597
                $newTotal = $cond->applyCondition($toBeCalculated);
598
599
                $process++;
600
601
            });
602
603
        return Helpers::formatValue($newTotal, $this->config['format_numbers'], $this->config);
604
    }
605
606
    /**
607
     * get total quantity of items in the cart
608
     *
609
     * @return int
610
     */
611
    public function getTotalQuantity()
612
    {
613
        $items = $this->getContent();
614
615
        if ($items->isEmpty()) return 0;
616
617
        $count = $items->sum(function ($item) {
618
            return $item['quantity'];
619
        });
620
621
        return $count;
622
    }
623
624
    /**
625
     * get the cart
626
     *
627
     * @return CartCollection
628
     */
629
    public function getContent()
630
    {
631
        return (new CartCollection($this->session->get($this->sessionKeyCartItems)));
632
    }
633
634
    /**
635
     * check if cart is empty
636
     *
637
     * @return bool
638
     */
639
    public function isEmpty()
640
    {
641
        $cart = new CartCollection($this->session->get($this->sessionKeyCartItems));
642
643
        return $cart->isEmpty();
644
    }
645
646
    /**
647
     * validate Item data
648
     *
649
     * @param $item
650
     * @return array $item;
651
     * @throws InvalidItemException
652
     */
653 View Code Duplication
    protected function validate($item)
654
    {
655
        $rules = array(
656
            'id' => 'required',
657
            'price' => 'required|numeric',
658
            'quantity' => 'required|numeric|min:1',
659
            'name' => 'required',
660
        );
661
662
        $validator = CartItemValidator::make($item, $rules);
663
664
        if ($validator->fails()) {
665
            throw new InvalidItemException($validator->messages()->first());
666
        }
667
668
        return $item;
669
    }
670
671
    /**
672
     * add row to cart collection
673
     *
674
     * @param $id
675
     * @param $item
676
     * @return bool
677
     */
678
    protected function addRow($id, $item)
679
    {
680
        if($this->fireEvent('adding', $item) === false) {
681
            return false;
682
        }
683
684
        $cart = $this->getContent();
685
686
        $cart->put($id, new ItemCollection($item, $this->config));
687
688
        $this->save($cart);
689
690
        $this->fireEvent('added', $item);
691
692
        return true;
693
    }
694
695
    /**
696
     * save the cart
697
     *
698
     * @param $cart CartCollection
699
     */
700
    protected function save($cart)
701
    {
702
        $this->session->put($this->sessionKeyCartItems, $cart);
703
    }
704
705
    /**
706
     * save the cart conditions
707
     *
708
     * @param $conditions
709
     */
710
    protected function saveConditions($conditions)
711
    {
712
        $this->session->put($this->sessionKeyCartConditions, $conditions);
713
    }
714
715
    /**
716
     * check if an item has condition
717
     *
718
     * @param $item
719
     * @return bool
720
     */
721
    protected function itemHasConditions($item)
722
    {
723
        if (!isset($item['conditions'])) return false;
724
725
        if (is_array($item['conditions'])) {
726
            return count($item['conditions']) > 0;
727
        }
728
729
        $conditionInstance = "Darryldecode\\Cart\\CartCondition";
730
731
        if ($item['conditions'] instanceof $conditionInstance) return true;
732
733
        return false;
734
    }
735
736
    /**
737
     * update a cart item quantity relative to its current quantity
738
     *
739
     * @param $item
740
     * @param $key
741
     * @param $value
742
     * @return mixed
743
     */
744
    protected function updateQuantityRelative($item, $key, $value)
745
    {
746
        if (preg_match('/\-/', $value) == 1) {
747
            $value = (int)str_replace('-', '', $value);
748
749
            // we will not allowed to reduced quantity to 0, so if the given value
750
            // would result to item quantity of 0, we will not do it.
751
            if (($item[$key] - $value) > 0) {
752
                $item[$key] -= $value;
753
            }
754
        } elseif (preg_match('/\+/', $value) == 1) {
755
            $item[$key] += (int)str_replace('+', '', $value);
756
        } else {
757
            $item[$key] += (int)$value;
758
        }
759
760
        return $item;
761
    }
762
763
    /**
764
     * update cart item quantity not relative to its current quantity value
765
     *
766
     * @param $item
767
     * @param $key
768
     * @param $value
769
     * @return mixed
770
     */
771
    protected function updateQuantityNotRelative($item, $key, $value)
772
    {
773
        $item[$key] = (int)$value;
774
775
        return $item;
776
    }
777
778
    /**
779
     * Setter for decimals. Change value on demand.
780
     * @param $decimals
781
     */
782
    public function setDecimals($decimals)
783
    {
784
        $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...
785
    }
786
787
    /**
788
     * Setter for decimals point. Change value on demand.
789
     * @param $dec_point
790
     */
791
    public function setDecPoint($dec_point)
792
    {
793
        $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...
794
    }
795
796
    public function setThousandsSep($thousands_sep)
797
    {
798
        $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...
799
    }
800
801
    /**
802
     * @param $name
803
     * @param $value
804
     * @return mixed
805
     */
806
    protected function fireEvent($name, $value = [])
807
    {
808
        return $this->events->fire($this->getInstanceName() . '.' . $name, array_values([$value, $this]));
809
    }
810
}
811