Completed
Push — master ( 3b65eb...dc665d )
by Nicolaas
11:00 queued 02:51
created

code/api/ShoppingCart.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * ShoppingCart - provides a global way to interface with the cart (current order).
4
 *
5
 * This can be used in other code by calling $cart = ShoppingCart::singleton();
6
 *
7
 * The shopping cart can be accessed as an order handler from the back-end
8
 * (e.g. when creating an order programmatically), while the accompagnying controller
9
 * is used by web-users to manipulate their order.
10
 *
11
 * A bunch of core functions are also stored in the order itself.
12
 * Methods and variables are in the shopping cart if they are relevant
13
 * only before (and while) the order is placed (e.g. latest update message),
14
 * and others are in the order because they are relevant even after the
15
 * order has been submitted (e.g. Total Cost).
16
 *
17
 * Key methods:
18
 *
19
 * //get Cart
20
 * $myCart = ShoppingCart::singleton();
21
 *
22
 * //get order
23
 * $myOrder = ShoppingCart::current_order();
24
 *
25
 * //view order (from another controller)
26
 * $this->redirect(ShoppingCart::current_order()->Link());
27
 *
28
 * //add item to cart
29
 * ShoppingCart::singleton()->addBuyable($myProduct);
30
 *
31
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
32
 * @package: ecommerce
33
 * @sub-package: control
34
 * @inspiration: Silverstripe Ltd, Jeremy
35
 **/
36
class ShoppingCart extends Object
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
37
{
38
    /**
39
     * List of names that can be used as session variables.
40
     * Also @see ShoppingCart::sessionVariableName.
41
     *
42
     * @var array
43
     */
44
    private static $session_variable_names = array('OrderID', 'Messages');
45
46
    /**
47
     * This is where we hold the (singleton) Shoppingcart.
48
     *
49
     * @var object (ShoppingCart)
50
     */
51
    private static $_singletoncart = null;
52
53
    /**
54
     * Feedback message to user (e.g. cart updated, could not delete item, someone in standing behind you).
55
     *
56
     *@var array
57
     **/
58
    protected $messages = array();
59
60
    /**
61
     * stores a reference to the current order object.
62
     *
63
     * @var object
64
     **/
65
    protected $order = null;
66
67
    /**
68
     * This variable is set to YES when we actually need an order (i.e. write it).
69
     *
70
     * @var bool
71
     */
72
    protected $requireSavedOrder = false;
73
74
    /**
75
     * Allows access to the cart from anywhere in code.
76
     *
77
     * @return ShoppingCart Object
78
     */
79
    public static function singleton()
80
    {
81
        if (!self::$_singletoncart) {
82
            self::$_singletoncart = Injector::inst()->get('ShoppingCart');
83
        }
84
85
        return self::$_singletoncart;
86
    }
87
88
    /**
89
     * Allows access to the current order from anywhere in the code..
90
     *
91
     * @return Order
92
     */
93
    public static function current_order()
94
    {
95
        return self::singleton()->currentOrder();
96
    }
97
98
    /**
99
     * looks up current order id.
100
     * you may supply an ID here, so that it looks up the current order ID
101
     * only when none is supplied.
102
     *
103
     * @param int (optional) $orderID
104
     *
105
     * @return int;
106
     */
107
    public static function current_order_id($orderID = 0)
108
    {
109
        if (!$orderID) {
110
            $order = self::current_order();
111
            if ($order && $order->exists()) {
112
                $orderID = $order->ID;
113
            }
114
        }
115
116
        return $orderID;
117
    }
118
119
    /**
120
     * Allows access to the current order from anywhere in the code..
121
     *
122
     * @return Order
123
     */
124
    public static function session_order()
125
    {
126
        $sessionVariableName = self::singleton()->sessionVariableName('OrderID');
127
        $orderIDFromSession = intval(Session::get($sessionVariableName)) - 0;
128
129
        return Order::get()->byID($orderIDFromSession);
130
    }
131
132
    /**
133
     * Gets or creates the current order.
134
     * Based on the session ONLY!
135
     * IMPORTANT FUNCTION!
136
     *
137
     * @todo - does this need to be public????
138
     *
139
     * @return Order
140
     */
141
    public function currentOrder($recurseCount = 0)
142
    {
143
        if (!$this->order) {
144
            $this->order = self::session_order();
145
            $loggedInMember = Member::currentUser();
146
            if ($this->order) {
147
                //first reason to set to null: it is already submitted
148
                if ($this->order->IsSubmitted()) {
149
                    $this->order = null;
150
                }
151
                //second reason to set to null: make sure we have permissions
152
                elseif (!$this->order->canView()) {
153
                    $this->order = null;
154
                }
155
                //logged in, add Member.ID to order->MemberID
156
                elseif ($loggedInMember && $loggedInMember->exists()) {
157
                    if ($this->order->MemberID != $loggedInMember->ID) {
158
                        $updateMember = false;
159
                        if (!$this->order->MemberID) {
160
                            $updateMember = true;
161
                        }
162
                        if (!$loggedInMember->IsShopAdmin()) {
163
                            $updateMember = true;
164
                        }
165
                        if ($updateMember) {
166
                            $this->order->MemberID = $loggedInMember->ID;
167
                            $this->order->write();
168
                        }
169
                    }
170
                    //IF current order has nothing in it AND the member already has an order: use the old one first
171
                    //first, lets check if the current order is worthwhile keeping
172
                    if ($this->order->StatusID || $this->order->TotalItems()) {
173
                        //do NOTHING!
174
                    } else {
175
                        $firstStep = OrderStep::get()->First();
176
                        //we assume the first step always exists.
177
                        //TODO: what sort order?
178
                        $count = 0;
179
                        while (
180
                            $firstStep &&
181
                            $previousOrderFromMember = Order::get()
182
                                ->where('
183
                                    "MemberID" = '.$loggedInMember->ID.'
184
                                    AND ("StatusID" = '.$firstStep->ID.' OR "StatusID" = 0)
185
                                    AND "Order"."ID" <> '.$this->order->ID
186
                                )
187
                                ->First()
188
                        ) {
189
                            //arbritary 12 attempts ...
190
                            if ($count > 12) {
191
                                break;
192
                            }
193
                            ++$count;
194
                            if ($previousOrderFromMember && $previousOrderFromMember->canView()) {
195
                                if ($previousOrderFromMember->StatusID || $previousOrderFromMember->TotalItems()) {
196
                                    $this->order->delete();
197
                                    $this->order = $previousOrderFromMember;
198
                                    break;
199
                                } else {
200
                                    $previousOrderFromMember->delete();
201
                                }
202
                            }
203
                        }
204
                    }
205
                }
206
            }
207
            if (!$this->order) {
208
                if ($loggedInMember) {
209
                    //find previour order...
210
                    $firstStep = OrderStep::get()->First();
211
                    if ($firstStep) {
212
                        $previousOrderFromMember = Order::get()
213
                            ->filter(array(
214
                                'MemberID' => $loggedInMember->ID,
215
                                'StatusID' => array($firstStep->ID, 0),
216
                            ))
217
                            ->First();
218
                        if ($previousOrderFromMember) {
219
                            if ($previousOrderFromMember->canView()) {
220
                                $this->order = $previousOrderFromMember;
221
                            }
222
                        }
223
                    }
224
                }
225
                if ($this->order && !$this->order->exists()) {
226
                    $this->order = null;
227
                }
228
                if (!$this->order) {
229
                    //here we cleanup old orders, because they should be
230
                    //cleaned at the same rate that they are created...
231
                    if (EcommerceConfig::get('ShoppingCart', 'cleanup_every_time')) {
232
                        $cartCleanupTask = EcommerceTaskCartCleanup::create();
233
                        $cartCleanupTask->runSilently();
234
                    }
235
                    //create new order
236
                    $this->order = Order::create();
237
                    if ($loggedInMember) {
238
                        $this->order->MemberID = $loggedInMember->ID;
239
                    }
240
                    $this->order->write();
241
                }
242
                $sessionVariableName = $this->sessionVariableName('OrderID');
243
                Session::set($sessionVariableName, intval($this->order->ID));
244
            }
245
            if ($this->order && $this->order->exists()) {
246
                $this->order->calculateOrderAttributes($force = false);
247
            }
248
            if ($this->order && !$this->order->SessionID) {
249
                //add session ID...
250
                $this->order->write();
251
            }
252
        }
253
        //try it again
254
        //but limit to three, just in case ...
255
        //just in case ...
256
        if (!$this->order && $recurseCount < 3) {
257
            ++$recurseCount;
258
259
            return $this->currentOrder();
260
        }
261
262
        return $this->order;
263
    }
264
265
    /**
266
     * Allows access to the current order from anywhere in the code..
267
     *
268
     * @return ShoppingCart Object
269
     */
270
    public function Link()
271
    {
272
        $order = self::singleton()->currentOrder();
273
        if ($order) {
274
            return $order->Link();
275
        }
276
    }
277
278
    /**
279
     * Adds any number of items to the cart.
280
     * Returns the order item on succes OR false on failure.
281
     *
282
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
283
     * @param float      $quantity   - number of items add.
284
     * @param mixed      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
285
     *                                 if you make it a form, it will save the form into the orderitem
286
     *
287
     * @return false | DataObject (OrderItem)
288
     */
289
    public function addBuyable(BuyableModel $buyable, $quantity = 1, $parameters = array())
290
    {
291
        if (!$buyable) {
292
            $this->addMessage(_t('Order.ITEMCOULDNOTBEFOUND', 'This item could not be found.'), 'bad');
293
            return false;
294
        }
295
        if (!$buyable->canPurchase()) {
296
            $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'This item is not for sale.'), 'bad');
297
            return false;
298
        }
299
        $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
300
        $quantity = $this->prepareQuantity($buyable, $quantity);
301
        if ($item && $quantity) { //find existing order item or make one
302
            $item->Quantity += $quantity;
303
            $item->write();
304
            $this->currentOrder()->Attributes()->add($item); //save to current order
305
            //TODO: distinquish between incremented and set
306
            //TODO: use sprintf to allow product name etc to be included in message
307
            if ($quantity > 1) {
308
                $msg = _t('Order.ITEMSADDED', 'Items added.');
309
            } else {
310
                $msg = _t('Order.ITEMADDED', 'Item added.');
311
            }
312
            $this->addMessage($msg, 'good');
313
        } elseif (!$item) {
314
            $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
315
        } else {
316
            $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'Item could not be added.'), 'bad');
317
        }
318
319
        return $item;
320
    }
321
322
    /**
323
     * Sets quantity for an item in the cart.
324
     *
325
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
326
     * @param float      $quantity   - number of items add.
327
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
328
     *
329
     * @return false | DataObject (OrderItem)
330
     */
331
    public function setQuantity(BuyableModel $buyable, $quantity, array $parameters = array())
332
    {
333
        $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
334
        $quantity = $this->prepareQuantity($buyable, $quantity);
335
        if ($item) {
336
            $item->Quantity = $quantity; //remove quantity
337
            $item->write();
338
            $this->addMessage(_t('Order.ITEMUPDATED', 'Item updated.'), 'good');
339
340
            return $item;
341
        } else {
342
            $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
343
        }
344
345
        return false;
346
    }
347
348
    /**
349
     * Removes any number of items from the cart.
350
     *
351
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
352
     * @param float      $quantity   - number of items add.
353
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
354
     *
355
     * @return false | DataObject (OrderItem)
356
     */
357
    public function decrementBuyable(BuyableModel $buyable, $quantity = 1, array $parameters = array())
358
    {
359
        $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
360
        $quantity = $this->prepareQuantity($buyable, $quantity);
361
        if ($item) {
362
            $item->Quantity -= $quantity; //remove quantity
363
            if ($item->Quantity < 0) {
364
                $item->Quantity = 0;
365
            }
366
            $item->write();
367
            if ($quantity > 1) {
368
                $msg = _t('Order.ITEMSREMOVED', 'Items removed.');
369
            } else {
370
                $msg = _t('Order.ITEMREMOVED', 'Item removed.');
371
            }
372
            $this->addMessage($msg, 'good');
373
374
            return $item;
375
        } else {
376
            $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
377
        }
378
379
        return false;
380
    }
381
382
    /**
383
     * Delete item from the cart.
384
     *
385
     * @param OrderItem $buyable    - the buyable (generally a product) being added to the cart
386
     * @param array     $parameters - array of parameters to target a specific order item. eg: group=1, length=5
387
     *
388
     * @return bool | item - successfully removed
389
     */
390
    public function deleteBuyable(BuyableModel $buyable, array $parameters = array())
391
    {
392
        $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = true);
393
        if ($item) {
394
            $this->currentOrder()->Attributes()->remove($item);
395
            $item->delete();
396
            $item->destroy();
397
            $this->addMessage(_t('Order.ITEMCOMPLETELYREMOVED', 'Item removed from cart.'), 'good');
398
399
            return $item;
400
        } else {
401
            $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
402
403
            return false;
404
        }
405
    }
406
407
    /**
408
     * Checks and prepares variables for a quantity change (add, edit, remove) for an Order Item.
409
     *
410
     * @param DataObject    $buyable             - the buyable (generally a product) being added to the cart
411
     * @param float         $quantity            - number of items add.
412
     * @param bool          $mustBeExistingItems - if false, the Order Item gets created if it does not exist - if TRUE the order item is searched for and an error shows if there is no Order item.
413
     * @param array | Form  $parameters          - array of parameters to target a specific order item. eg: group=1, length=5*
414
     *                                           - form saved into item...
415
     *
416
     * @return bool | DataObject ($orderItem)
417
     */
418
    protected function prepareOrderItem(BuyableModel $buyable, $parameters = array(), $mustBeExistingItem = true)
419
    {
420
        $parametersArray = $parameters;
421
        $form = null;
422
        if ($parameters instanceof Form) {
423
            $parametersArray = array();
424
            $form = $parameters;
425
        }
426
        if (!$buyable) {
427
            user_error('No buyable was provided', E_USER_WARNING);
428
        }
429
        if (!$buyable->canPurchase()) {
430
            $item = $this->getExistingItem($buyable, $parametersArray);
431
            if ($item && $item->exists()) {
432
                $item->delete();
433
                $item->destroy();
434
            }
435
436
            return false;
437
        }
438
        $item = null;
439
        if ($mustBeExistingItem) {
440
            $item = $this->getExistingItem($buyable, $parametersArray);
441
        } else {
442
            $item = $this->findOrMakeItem($buyable, $parametersArray); //find existing order item or make one
443
        }
444
        if (!$item) {
445
            //check for existence of item
446
            return false;
447
        }
448
        if ($form) {
449
            $form->saveInto($item);
450
        }
451
452
        return $item;
453
    }
454
455
    /**
456
     * @todo: what does this method do???
457
     *
458
     * @return int
459
     *
460
     * @param DataObject ($buyable)
461
     * @param float $quantity
462
     */
463
    protected function prepareQuantity(BuyableModel $buyable, $quantity)
464
    {
465
        $quantity = round($quantity, $buyable->QuantityDecimals());
466
        if ($quantity < 0 || (!$quantity && $quantity !== 0)) {
467
            $this->addMessage(_t('Order.INVALIDQUANTITY', 'Invalid quantity.'), 'warning');
468
469
            return false;
470
        }
471
472
        return $quantity;
473
    }
474
475
    /**
476
     * Helper function for making / retrieving order items.
477
     * we do not need things like "canPurchase" here, because that is with the "addBuyable" method.
478
     * NOTE: does not write!
479
     *
480
     * @param DataObject $buyable
481
     * @param array      $parameters
482
     *
483
     * @return OrderItem
484
     */
485
    public function findOrMakeItem(BuyableModel $buyable, array $parameters = array())
486
    {
487
        if ($item = $this->getExistingItem($buyable, $parameters)) {
488
            //do nothing
489
        } else {
490
            //otherwise create a new item
491
            if (!($buyable instanceof BuyableModel)) {
492
                $this->addMessage(_t('ShoppingCart.ITEMNOTFOUND', 'Item is not buyable.'), 'bad');
493
494
                return false;
495
            }
496
            $className = $buyable->classNameForOrderItem();
497
            $item = new $className();
498
            if ($order = $this->currentOrder()) {
499
                $item->OrderID = $order->ID;
500
                $item->BuyableID = $buyable->ID;
501
                $item->BuyableClassName = $buyable->ClassName;
502
                if (isset($buyable->Version)) {
503
                    $item->Version = $buyable->Version;
504
                }
505
            }
506
        }
507
        if ($parameters) {
508
            $item->Parameters = $parameters;
509
        }
510
511
        return $item;
512
    }
513
514
    /**
515
     * submit the order so that it is no longer available
516
     * in the cart but will continue its journey through the
517
     * order steps.
518
     *
519
     * @return bool
520
     */
521
    public function submit()
522
    {
523
        $this->currentOrder()->tryToFinaliseOrder();
524
        $this->clear();
525
        //little hack to clear static memory
526
        OrderItem::reset_price_has_been_fixed($this->currentOrder()->ID);
527
528
        return true;
529
    }
530
531
    /**
532
     * @return bool
533
     */
534
    public function save()
535
    {
536
        $this->currentOrder()->write();
537
        $this->addMessage(_t('Order.ORDERSAVED', 'Order Saved.'), 'good');
538
539
        return true;
540
    }
541
542
    /**
543
     * Clears the cart contents completely by removing the orderID from session, and
544
     * thus creating a new cart on next request.
545
     *
546
     * @return bool
547
     */
548
    public function clear()
549
    {
550
        //we keep this here so that a flush can be added...
551
        set_time_limit(1 * 60);
552
        self::$_singletoncart = null;
553
        $this->order = null;
554
        $this->messages = array();
555
        foreach (self::$session_variable_names as $name) {
556
            $sessionVariableName = $this->sessionVariableName($name);
557
            Session::set($sessionVariableName, null);
558
            Session::clear($sessionVariableName);
559
            Session::save();
560
        }
561
        $memberID = Intval(Member::currentUserID());
562
        if ($memberID) {
563
            $orders = Order::get()->filter(array('MemberID' => $memberID));
564
            if ($orders && $orders->count()) {
565
                foreach ($orders as $order) {
566
                    if (!$order->IsSubmitted()) {
567
                        $order->delete();
568
                    }
569
                }
570
            }
571
        }
572
573
        return true;
574
    }
575
576
    /**
577
     * alias for clear.
578
     */
579
    public function reset()
580
    {
581
        return $this->clear();
582
    }
583
584
    /**
585
     * Removes a modifier from the cart
586
     * It does not actually remove it, but it just
587
     * sets it as "removed", to avoid that it is being
588
     * added again.
589
     *
590
     * @param OrderModifier $modifier
591
     *
592
     * @return bool
593
     */
594
    public function removeModifier(OrderModifier $modifier)
595
    {
596
        $modifier = (is_numeric($modifier)) ? OrderModifier::get()->byID($modifier) : $modifier;
597
        if (!$modifier) {
598
            $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
599
600
            return false;
601
        }
602
        if (!$modifier->CanBeRemoved()) {
603
            $this->addMessage(_t('Order.MODIFIERCANNOTBEREMOVED', 'Modifier can not be removed.'), 'bad');
604
605
            return false;
606
        }
607
        $modifier->HasBeenRemoved = 1;
608
        $modifier->onBeforeRemove();
609
        $modifier->write();
610
        $modifier->onAfterRemove();
611
        $this->addMessage(_t('Order.MODIFIERREMOVED', 'Removed.'), 'good');
612
613
        return true;
614
    }
615
616
    /**
617
     * Removes a modifier from the cart.
618
     *
619
     * @param Int/ OrderModifier
620
     *
621
     * @return bool
622
     */
623
    public function addModifier($modifier)
624
    {
625
        if (is_numeric($modifier)) {
626
            $modifier = OrderModifier::get()->byID($modifier);
627
        } elseif (!(is_a($modifier, Object::getCustomClass('OrderModifier')))) {
628
            user_error('Bad parameter provided to ShoppingCart::addModifier', E_USER_WARNING);
629
        }
630
        if (!$modifier) {
631
            $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
632
633
            return false;
634
        }
635
        $modifier->HasBeenRemoved = 0;
636
        $modifier->write();
637
        $this->addMessage(_t('Order.MODIFIERREMOVED', 'Added.'), 'good');
638
639
        return true;
640
    }
641
642
    /**
643
     * Sets an order as the current order.
644
     *
645
     * @param int | Order $order
646
     *
647
     * @return bool
648
     */
649
    public function loadOrder($order)
650
    {
651
        //TODO: how to handle existing order
652
        //TODO: permission check - does this belong to another member? ...or should permission be assumed already?
653
        if (is_numeric($order)) {
654
            $this->order = Order::get()->byID($order);
655
        } elseif (is_a($order, Object::getCustomClass('Order'))) {
656
            $this->order = $order;
657
        } else {
658
            user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
659
        }
660
        if ($this->order) {
661
            //first can view and then, if can view, set as session...
662
            if ($this->order->canView()) {
663
                $this->order->init(true);
664
                $sessionVariableName = $this->sessionVariableName('OrderID');
665
                //we set session ID after can view check ...
666
                Session::set($sessionVariableName, $this->order->ID);
667
                $this->addMessage(_t('Order.LOADEDEXISTING', 'Order loaded.'), 'good');
668
669
                return true;
670
            } else {
671
                $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
672
673
                return false;
674
            }
675
        } else {
676
            $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
677
678
            return false;
679
        }
680
    }
681
682
    /**
683
     * NOTE: tried to copy part to the Order Class - but that was not much of a go-er.
684
     *
685
     * @param int | Order $order
686
     *
687
     * @return Order | false
688
     **/
689
    public function copyOrder($oldOrder)
690
    {
691
        if (is_numeric($oldOrder)) {
692
            $oldOrder = Order::get()->byID(intval($oldOrder));
693
        } elseif (is_a($oldOrder, Object::getCustomClass('Order'))) {
694
            //$oldOrder = $oldOrder;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
695
        } else {
696
            user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
697
        }
698
        if ($oldOrder) {
699
            if ($oldOrder->canView() && $oldOrder->IsSubmitted()) {
700
                $newOrder = Order::create();
701
                //copying fields.
702
                $newOrder->UseShippingAddress = $oldOrder->UseShippingAddress;
703
                //important to set it this way...
704
                $newOrder->setCurrency($oldOrder->CurrencyUsed());
705
                $newOrder->MemberID = $oldOrder->MemberID;
706
                //load the order
707
                $newOrder->write();
708
                $this->loadOrder($newOrder);
709
                $items = OrderItem::get()
710
                    ->filter(array('OrderID' => $oldOrder->ID));
711
                if ($items->count()) {
712
                    foreach ($items as $item) {
713
                        $buyable = $item->Buyable($current = true);
714
                        if ($buyable->canPurchase()) {
715
                            $this->addBuyable($buyable, $item->Quantity);
716
                        }
717
                    }
718
                }
719
                $newOrder->CreateOrReturnExistingAddress('BillingAddress');
720
                $newOrder->CreateOrReturnExistingAddress('ShippingAddress');
721
                $newOrder->write();
722
                $this->addMessage(_t('Order.ORDERCOPIED', 'Order has been copied.'), 'good');
723
724
                return $newOrder;
725
            } else {
726
                $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
727
728
                return false;
729
            }
730
        } else {
731
            $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
732
733
            return false;
734
        }
735
    }
736
737
    /**
738
     * sets country in order so that modifiers can be recalculated, etc...
739
     *
740
     * @param string - $countryCode
741
     *
742
     * @return bool
743
     **/
744
    public function setCountry($countryCode)
745
    {
746
        if (EcommerceCountry::code_allowed($countryCode)) {
747
            $this->currentOrder()->SetCountryFields($countryCode);
748
            $this->addMessage(_t('Order.UPDATEDCOUNTRY', 'Updated country.'), 'good');
749
750
            return true;
751
        } else {
752
            $this->addMessage(_t('Order.NOTUPDATEDCOUNTRY', 'Could not update country.'), 'bad');
753
754
            return false;
755
        }
756
    }
757
758
    /**
759
     * sets region in order so that modifiers can be recalculated, etc...
760
     *
761
     * @param int | String - $regionID you can use the ID or the code.
762
     *
763
     * @return bool
764
     **/
765
    public function setRegion($regionID)
766
    {
767
        if (EcommerceRegion::regionid_allowed($regionID)) {
768
            $this->currentOrder()->SetRegionFields($regionID);
769
            $this->addMessage(_t('ShoppingCart.REGIONUPDATED', 'Region updated.'), 'good');
770
771
            return true;
772
        } else {
773
            $this->addMessage(_t('ORDER.NOTUPDATEDREGION', 'Could not update region.'), 'bad');
774
775
            return false;
776
        }
777
    }
778
779
    /**
780
     * sets the display currency for the cart.
781
     *
782
     * @param string $currencyCode
783
     *
784
     * @return bool
785
     **/
786
    public function setCurrency($currencyCode)
787
    {
788
        $currency = EcommerceCurrency::get_one_from_code($currencyCode);
789
        if ($currency) {
790
            if ($this->currentOrder()->MemberID) {
791
                $member = $this->currentOrder()->Member();
792
                if ($member && $member->exists()) {
793
                    $member->SetPreferredCurrency($currency);
794
                }
795
            }
796
            $this->currentOrder()->UpdateCurrency($currency);
797
            $msg = _t('Order.CURRENCYUPDATED', 'Currency updated.');
798
            $this->addMessage($msg, 'good');
799
800
            return true;
801
        } else {
802
            $msg = _t('Order.CURRENCYCOULDNOTBEUPDATED', 'Currency could not be updated.');
803
            $this->addMessage($msg, 'bad');
804
805
            return false;
806
        }
807
    }
808
809
    /**
810
     * Produces a debug of the shopping cart.
811
     */
812
    public function debug()
813
    {
814
        if (Director::isDev() || Permission::check('ADMIN')) {
815
            debug::show($this->currentOrder());
816
817
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Country</h1>';
818
            echo 'GEOIP Country: '.EcommerceCountry::get_country_from_ip().'<br />';
819
            echo 'Calculated Country Country: '.EcommerceCountry::get_country().'<br />';
820
821
            echo '<blockquote><blockquote><blockquote><blockquote>';
822
823
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Items</h1>';
824
            $items = $this->currentOrder()->Items();
825
            echo $items->sql();
826
            echo '<hr />';
827
            if ($items->count()) {
828
                foreach ($items as $item) {
829
                    Debug::show($item);
830
                }
831
            } else {
832
                echo '<p>there are no items for this order</p>';
833
            }
834
835
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Modifiers</h1>';
836
            $modifiers = $this->currentOrder()->Modifiers();
837
            if ($modifiers->count()) {
838
                foreach ($modifiers as $modifier) {
839
                    Debug::show($modifier);
840
                }
841
            } else {
842
                echo '<p>there are no modifiers for this order</p>';
843
            }
844
845
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Addresses</h1>';
846
            $billingAddress = $this->currentOrder()->BillingAddress();
847
            if ($billingAddress && $billingAddress->exists()) {
848
                Debug::show($billingAddress);
849
            } else {
850
                echo '<p>there is no billing address for this order</p>';
851
            }
852
            $shippingAddress = $this->currentOrder()->ShippingAddress();
853
            if ($shippingAddress && $shippingAddress->exists()) {
854
                Debug::show($shippingAddress);
855
            } else {
856
                echo '<p>there is no shipping address for this order</p>';
857
            }
858
859
            $currencyUsed = $this->currentOrder()->CurrencyUsed();
860
            if ($currencyUsed && $currencyUsed->exists()) {
861
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Currency</h1>';
862
                Debug::show($currencyUsed);
863
            }
864
865
            $cancelledBy = $this->currentOrder()->CancelledBy();
866
            if ($cancelledBy && $cancelledBy->exists()) {
867
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Cancelled By</h1>';
868
                Debug::show($cancelledBy);
869
            }
870
871
            $logs = $this->currentOrder()->OrderStatusLogs();
872
            if ($logs && $logs->count()) {
873
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Logs</h1>';
874
                foreach ($logs as $log) {
875
                    Debug::show($log);
876
                }
877
            }
878
879
            $payments = $this->currentOrder()->Payments();
880
            if ($payments  && $payments->count()) {
881
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Payments</h1>';
882
                foreach ($payments as $payment) {
883
                    Debug::show($payment);
884
                }
885
            }
886
887
            $emails = $this->currentOrder()->Emails();
888
            if ($emails && $emails->count()) {
889
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Emails</h1>';
890
                foreach ($emails as $email) {
891
                    Debug::show($email);
892
                }
893
            }
894
895
            echo '</blockquote></blockquote></blockquote></blockquote>';
896
        } else {
897
            echo 'Please log in as admin first';
898
        }
899
    }
900
901
    /**
902
     * Stores a message that can later be returned via ajax or to $form->sessionMessage();.
903
     *
904
     * @param $message - the message, which could be a notification of successful action, or reason for failure
905
     * @param $type - please use good, bad, warning
906
     */
907
    public function addMessage($message, $status = 'good')
908
    {
909
        //clean status for the lazy programmer
910
        //TODO: remove the awkward replace
911
        $status = strtolower($status);
912
        str_replace(array('success', 'failure'), array('good', 'bad'), $status);
913
        $statusOptions = array('good', 'bad', 'warning');
914
        if (!in_array($status, $statusOptions)) {
915
            user_error('Message status should be one of the following: '.implode(',', $statusOptions), E_USER_NOTICE);
916
        }
917
        $this->messages[] = array(
918
            'Message' => $message,
919
            'Type' => $status,
920
        );
921
    }
922
923
    /*******************************************************
924
    * HELPER FUNCTIONS
925
    *******************************************************/
926
927
    /**
928
     * Gets an existing order item based on buyable and passed parameters.
929
     *
930
     * @param DataObject $buyable
931
     * @param array      $parameters
932
     *
933
     * @return OrderItem | null
934
     */
935
    protected function getExistingItem(BuyableModel $buyable, array $parameters = array())
936
    {
937
        $filterString = $this->parametersToSQL($parameters);
938
        if ($order = $this->currentOrder()) {
939
            $orderID = $order->ID;
940
            $obj = OrderItem::get()
941
                ->where(
942
                    " \"BuyableClassName\" = '".$buyable->ClassName."' AND
943
                    \"BuyableID\" = ".$buyable->ID.' AND
944
                    "OrderID" = '.$orderID.' '.
945
                    $filterString
946
                )
947
                ->First();
948
949
            return $obj;
950
        }
951
    }
952
953
    /**
954
     * Removes parameters that aren't in the default array, merges with default parameters, and converts raw2SQL.
955
     *
956
     * @param array $parameters
957
     *
958
     * @return cleaned array
959
     */
960
    protected function cleanParameters(array $params = array())
961
    {
962
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
963
        $newarray = array_merge(array(), $defaultParamFilters); //clone array
964
        if (!count($newarray)) {
965
            return array(); //no use for this if there are not parameters defined
966
        }
967
        foreach ($newarray as $field => $value) {
968
            if (isset($params[$field])) {
969
                $newarray[$field] = Convert::raw2sql($params[$field]);
970
            }
971
        }
972
973
        return $newarray;
974
    }
975
976
    /**
977
     * @param array $parameters
978
     *                          Converts parameter array to SQL query filter
979
     */
980
    protected function parametersToSQL(array $parameters = array())
981
    {
982
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
983
        if (!count($defaultParamFilters)) {
984
            return ''; //no use for this if there are not parameters defined
985
        }
986
        $cleanedparams = $this->cleanParameters($parameters);
987
        $outputArray = array();
988
        foreach ($cleanedparams as $field => $value) {
989
            $outputarray[$field] = '"'.$field.'" = '.$value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$outputarray was never initialized. Although not strictly required by PHP, it is generally a good practice to add $outputarray = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
990
        }
991
        if (count($outputArray)) {
992
            return implode(' AND ', $outputArray);
993
        }
994
995
        return '';
996
    }
997
998
    /*******************************************************
999
    * UI MESSAGE HANDLING
1000
    *******************************************************/
1001
1002
    /**
1003
     * Retrieves all good, bad, and ugly messages that have been produced during the current request.
1004
     *
1005
     * @return array of messages
1006
     */
1007
    public function getMessages()
1008
    {
1009
        $sessionVariableName = $this->sessionVariableName('Messages');
1010
        //get old messages
1011
        $messages = unserialize(Session::get($sessionVariableName));
1012
        //clear old messages
1013
        Session::clear($sessionVariableName, '');
1014
        //set to form????
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1015
        if ($messages && count($messages)) {
1016
            $this->messages = array_merge($messages, $this->messages);
1017
        }
1018
1019
        return $this->messages;
1020
    }
1021
1022
    /**
1023
     *Saves current messages in session for retrieving them later.
1024
     *
1025
     *@return array of messages
1026
     */
1027
    protected function StoreMessagesInSession()
1028
    {
1029
        $sessionVariableName = $this->sessionVariableName('Messages');
1030
        Session::set($sessionVariableName, serialize($this->messages));
1031
    }
1032
1033
    /**
1034
     * This method is used to return data after an ajax call was made.
1035
     * When a asynchronious request is made to the shopping cart (ajax),
1036
     * then you will first action the request and then use this function
1037
     * to return some values.
1038
     *
1039
     * It can also be used without ajax, in wich case it will redirects back
1040
     * to the last page.
1041
     *
1042
     * Note that you can set the ajax response class in the configuration file.
1043
     *
1044
     *
1045
     * @param string $message
1046
     * @param string $status
1047
     * @param Form   $form
1048
     * @returns String (JSON)
1049
     */
1050
    public function setMessageAndReturn($message = '', $status = '', Form $form = null)
1051
    {
1052
        if ($message && $status) {
1053
            $this->addMessage($message, $status);
1054
        }
1055
        //TODO: handle passing back multiple messages
1056
1057
        if (Director::is_ajax()) {
1058
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
1059
            $obj = new $responseClass();
1060
1061
            return $obj->ReturnCartData($this->getMessages());
1062
        } else {
1063
            //TODO: handle passing a message back to a form->sessionMessage
1064
            $this->StoreMessagesInSession();
1065
            if ($form) {
1066
                //lets make sure that there is an order
1067
                $this->currentOrder();
1068
                //nowe we can (re)calculate the order
1069
                $this->order->calculateOrderAttributes($force = false);
1070
                $form->sessionMessage($message, $status);
1071
                //let the form controller do the redirectback or whatever else is needed.
1072
            } else {
1073
                if (empty($_REQUEST['BackURL']) && Controller::has_curr()) {
1074
                    Controller::curr()->redirectBack();
1075
                } else {
1076
                    Controller::curr()->redirect(urldecode($_REQUEST['BackURL']));
1077
                }
1078
            }
1079
1080
            return;
1081
        }
1082
    }
1083
1084
    /**
1085
     * @return EcommerceDBConfig
1086
     */
1087
    protected function EcomConfig()
1088
    {
1089
        return EcommerceDBConfig::current_ecommerce_db_config();
1090
    }
1091
1092
    /**
1093
     * Return the name of the session variable that should be used.
1094
     *
1095
     * @param string $name
1096
     *
1097
     * @return string
1098
     */
1099
    protected function sessionVariableName($name = '')
1100
    {
1101
        if (!in_array($name, self::$session_variable_names)) {
1102
            user_error("Tried to set session variable $name, that is not in use", E_USER_NOTICE);
1103
        }
1104
        $sessionCode = EcommerceConfig::get('ShoppingCart', 'session_code');
1105
1106
        return $sessionCode.'_'.$name;
1107
    }
1108
}
1109