Completed
Pull Request — master (#76)
by
unknown
01:45
created

Cart::fireEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 2
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
513
     * @param bool $formatted
514
     * @return float
515
     */
516
    public function getSubTotal($formatted = true)
517
    {
518
        $cart = $this->getContent();
519
520
        $sum = $cart->sum(function ($item) {
521
            return $item->getPriceSumWithConditions(false);
522
        });
523
524
        return Helpers::formatValue(floatval($sum), $formatted, $this->config);
525
    }
526
527
    /**
528
     * the new total in which conditions are already applied
529
     *
530
     * @return float
531
     */
532
    public function getTotal()
533
    {
534
        $subTotal = $this->getSubTotal(false);
535
536
        $newTotal = 0.00;
537
538
        $process = 0;
539
540
        $conditions = $this->getConditions();
541
542
        // if no conditions were added, just return the sub total
543
        if (!$conditions->count()) {
544
            return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config);
545
        }
546
547
        $conditions->each(function ($cond) use ($subTotal, &$newTotal, &$process) {
548
            if ($cond->getTarget() === 'subtotal') {
549
                ($process > 0) ? $toBeCalculated = $newTotal : $toBeCalculated = $subTotal;
550
551
                $newTotal = $cond->applyCondition($toBeCalculated);
552
553
                $process++;
554
            }
555
        });
556
557
        return Helpers::formatValue($newTotal, $this->config['format_numbers'], $this->config);
558
    }
559
560
    /**
561
     * get total quantity of items in the cart
562
     *
563
     * @return int
564
     */
565
    public function getTotalQuantity()
566
    {
567
        $items = $this->getContent();
568
569
        if ($items->isEmpty()) return 0;
570
571
        $count = $items->sum(function ($item) {
572
            return $item['quantity'];
573
        });
574
575
        return $count;
576
    }
577
578
    /**
579
     * get the cart
580
     *
581
     * @return CartCollection
582
     */
583
    public function getContent()
584
    {
585
        return (new CartCollection($this->session->get($this->sessionKeyCartItems)));
586
    }
587
588
    /**
589
     * check if cart is empty
590
     *
591
     * @return bool
592
     */
593
    public function isEmpty()
594
    {
595
        $cart = new CartCollection($this->session->get($this->sessionKeyCartItems));
596
597
        return $cart->isEmpty();
598
    }
599
600
    /**
601
     * validate Item data
602
     *
603
     * @param $item
604
     * @return array $item;
605
     * @throws InvalidItemException
606
     */
607 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...
608
    {
609
        $rules = array(
610
            'id' => 'required',
611
            'price' => 'required|numeric',
612
            'quantity' => 'required|numeric|min:1',
613
            'name' => 'required',
614
        );
615
616
        $validator = CartItemValidator::make($item, $rules);
617
618
        if ($validator->fails()) {
619
            throw new InvalidItemException($validator->messages()->first());
620
        }
621
622
        return $item;
623
    }
624
625
    /**
626
     * add row to cart collection
627
     *
628
     * @param $id
629
     * @param $item
630
     * @return bool
631
     */
632
    protected function addRow($id, $item)
633
    {
634
        if($this->fireEvent('adding', $item) === false) {
635
            return false;
636
        }
637
638
        $cart = $this->getContent();
639
640
        $cart->put($id, new ItemCollection($item, $this->config));
641
642
        $this->save($cart);
643
644
        $this->fireEvent('added', $item);
645
646
        return true;
647
    }
648
649
    /**
650
     * save the cart
651
     *
652
     * @param $cart CartCollection
653
     */
654
    protected function save($cart)
655
    {
656
        $this->session->put($this->sessionKeyCartItems, $cart);
657
    }
658
659
    /**
660
     * save the cart conditions
661
     *
662
     * @param $conditions
663
     */
664
    protected function saveConditions($conditions)
665
    {
666
        $this->session->put($this->sessionKeyCartConditions, $conditions);
667
    }
668
669
    /**
670
     * check if an item has condition
671
     *
672
     * @param $item
673
     * @return bool
674
     */
675
    protected function itemHasConditions($item)
676
    {
677
        if (!isset($item['conditions'])) return false;
678
679
        if (is_array($item['conditions'])) {
680
            return count($item['conditions']) > 0;
681
        }
682
683
        $conditionInstance = "Darryldecode\\Cart\\CartCondition";
684
685
        if ($item['conditions'] instanceof $conditionInstance) return true;
686
687
        return false;
688
    }
689
690
    /**
691
     * update a cart item quantity relative to its current quantity
692
     *
693
     * @param $item
694
     * @param $key
695
     * @param $value
696
     * @return mixed
697
     */
698
    protected function updateQuantityRelative($item, $key, $value)
699
    {
700
        if (preg_match('/\-/', $value) == 1) {
701
            $value = (int)str_replace('-', '', $value);
702
703
            // we will not allowed to reduced quantity to 0, so if the given value
704
            // would result to item quantity of 0, we will not do it.
705
            if (($item[$key] - $value) > 0) {
706
                $item[$key] -= $value;
707
            }
708
        } elseif (preg_match('/\+/', $value) == 1) {
709
            $item[$key] += (int)str_replace('+', '', $value);
710
        } else {
711
            $item[$key] += (int)$value;
712
        }
713
714
        return $item;
715
    }
716
717
    /**
718
     * update cart item quantity not relative to its current quantity value
719
     *
720
     * @param $item
721
     * @param $key
722
     * @param $value
723
     * @return mixed
724
     */
725
    protected function updateQuantityNotRelative($item, $key, $value)
726
    {
727
        $item[$key] = (int)$value;
728
729
        return $item;
730
    }
731
732
    /**
733
     * Setter for decimals. Change value on demand.
734
     * @param $decimals
735
     */
736
    public function setDecimals($decimals)
737
    {
738
        $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...
739
    }
740
741
    /**
742
     * Setter for decimals point. Change value on demand.
743
     * @param $dec_point
744
     */
745
    public function setDecPoint($dec_point)
746
    {
747
        $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...
748
    }
749
750
    public function setThousandsSep($thousands_sep)
751
    {
752
        $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...
753
    }
754
755
    /**
756
     * @param $name
757
     * @param $value
758
     * @return mixed
759
     */
760
    protected function fireEvent($name, $value = [])
761
    {
762
        return $this->events->fire($this->getInstanceName() . '.' . $name, array_values([$value, $this]));
763
    }
764
}
765