Completed
Push — master ( ce2dd0...10a6ac )
by Nicolaas
04:02
created

code/api/ShoppingCart.php (47 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
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
     * if you do not like the session Order then you can set it here ...
92
     *
93
     * @param Order $order (optional)
0 ignored issues
show
Should the type for parameter $order not be Order|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
94
     *
95
     * @return Order
96
     */
97
    public static function current_order($order = null)
98
    {
99
        return self::singleton()->currentOrder(0, $order);
100
    }
101
102
    /**
103
     * looks up current order id.
104
     * you may supply an ID here, so that it looks up the current order ID
105
     * only when none is supplied.
106
     *
107
     * @param int (optional) | Order $orderOrOrderID
108
     *
109
     * @return int;
0 ignored issues
show
The doc-type int; could not be parsed: Expected "|" or "end of type", but got ";" at position 3. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
110
     */
111
    public static function current_order_id($orderOrOrderID = 0)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
112
    {
113
        $orderID = 0;
114
        if (!$orderOrOrderID) {
115
            $order = self::current_order();
116
            if ($order && $order->exists()) {
117
                $orderID = $order->ID;
118
            }
119
        }
120
        if($orderOrOrderID instanceof Order) {
121
            $orderID = $orderOrOrderID->ID;
122
        } elseif(intval($orderOrOrderID)) {
123
            $orderID = intval($orderOrOrderID);
124
        }
125
126
        return $orderID;
127
    }
128
129
    /**
130
     * Allows access to the current order from anywhere in the code..
131
     *
132
     * @return Order
0 ignored issues
show
Should the return type not be Order|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
133
     */
134
    public static function session_order()
135
    {
136
        $sessionVariableName = self::singleton()->sessionVariableName('OrderID');
137
        $orderIDFromSession = intval(Session::get($sessionVariableName)) - 0;
138
139
        return Order::get()->byID($orderIDFromSession);
140
    }
141
142
    /**
143
     * set a specific order, other than the one from session ....
144
     *
145
     * @param Order $order
146
     *
147
     * @return Order
148
     */
149
    public function setOrder($order)
150
    {
151
        $this->order = $order;
152
        return $this->order;
153
    }
154
155
    /**
156
     * Gets or creates the current order.
157
     * Based on the session ONLY unless the order has been explictely set.
158
     * IMPORTANT FUNCTION!
159
     *
160
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
161
     *
162
     * However, you can pass an order in case you want to manipulate an order that is not in sesssion
163
     *
164
     * @param int $recurseCount (optional)
165
     * @param Order $order (optional)
0 ignored issues
show
Should the type for parameter $order not be Order|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
166
     *
167
     * @return Order
168
     */
169
    public function currentOrder($recurseCount = 0, $order = null)
170
    {
171
        if($order) {
172
            $this->order = $order;
173
        }
174
        if($this->allowWrites()) {
175
            if (!$this->order) {
176
                $this->order = self::session_order();
177
                $loggedInMember = Member::currentUser();
178
                if ($this->order) {
179
                    //first reason to set to null: it is already submitted
180
                    if ($this->order->IsSubmitted()) {
181
                        $this->order = null;
182
                    }
183
                    //second reason to set to null: make sure we have permissions
184
                    elseif (!$this->order->canView()) {
185
                        $this->order = null;
186
                    }
187
                    //logged in, add Member.ID to order->MemberID
188
                    elseif ($loggedInMember && $loggedInMember->exists()) {
189
                        if ($this->order->MemberID != $loggedInMember->ID) {
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
190
                            $updateMember = false;
191
                            if (!$this->order->MemberID) {
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
192
                                $updateMember = true;
193
                            }
194
                            if (!$loggedInMember->IsShopAdmin()) {
195
                                $updateMember = true;
196
                            }
197
                            if ($updateMember) {
198
                                $this->order->MemberID = $loggedInMember->ID;
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
199
                                $this->order->write();
200
                            }
201
                        }
202
                        //IF current order has nothing in it AND the member already has an order: use the old one first
203
                        //first, lets check if the current order is worthwhile keeping
204
                        if ($this->order->StatusID || $this->order->TotalItems()) {
0 ignored issues
show
The property StatusID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
205
                            //do NOTHING!
206
                        } else {
207
                            $firstStep = OrderStep::get()->First();
208
                            //we assume the first step always exists.
209
                            //TODO: what sort order?
210
                            $count = 0;
211
                            while (
212
                                $firstStep &&
213
                                $previousOrderFromMember = Order::get()
214
                                    ->where('
215
                                        "MemberID" = '.$loggedInMember->ID.'
216
                                        AND ("StatusID" = '.$firstStep->ID.' OR "StatusID" = 0)
217
                                        AND "Order"."ID" <> '.$this->order->ID
218
                                    )
219
                                    ->First()
220
                            ) {
221
                                //arbritary 12 attempts ...
222
                                if ($count > 12) {
223
                                    break;
224
                                }
225
                                ++$count;
226
                                if ($previousOrderFromMember && $previousOrderFromMember->canView()) {
227
                                    if ($previousOrderFromMember->StatusID || $previousOrderFromMember->TotalItems()) {
228
                                        $this->order->delete();
229
                                        $this->order = $previousOrderFromMember;
230
                                        break;
231
                                    } else {
232
                                        $previousOrderFromMember->delete();
233
                                    }
234
                                }
235
                            }
236
                        }
237
                    }
238
                }
239
                if (! $this->order) {
240
                    if ($loggedInMember) {
241
                        //find previour order...
242
                        $firstStep = OrderStep::get()->First();
243
                        if ($firstStep) {
244
                            $previousOrderFromMember = Order::get()
245
                                ->filter(array(
246
                                    'MemberID' => $loggedInMember->ID,
247
                                    'StatusID' => array($firstStep->ID, 0),
248
                                ))
249
                                ->First();
250
                            if ($previousOrderFromMember) {
251
                                if ($previousOrderFromMember->canView()) {
252
                                    $this->order = $previousOrderFromMember;
253
                                }
254
                            }
255
                        }
256
                    }
257
                    if ($this->order && !$this->order->exists()) {
258
                        $this->order = null;
259
                    }
260
                    if (! $this->order) {
261
                        //here we cleanup old orders, because they should be
262
                        //cleaned at the same rate that they are created...
263
                        if (EcommerceConfig::get('ShoppingCart', 'cleanup_every_time')) {
264
                            $cartCleanupTask = EcommerceTaskCartCleanup::create();
265
                            $cartCleanupTask->runSilently();
266
                        }
267
                        //create new order
268
                        $this->order = Order::create();
269
                        if ($loggedInMember) {
270
                            $this->order->MemberID = $loggedInMember->ID;
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
271
                        }
272
                        $this->order->write();
273
                    }
274
                    $sessionVariableName = $this->sessionVariableName('OrderID');
275
                    Session::set($sessionVariableName, intval($this->order->ID));
276
                }
277
                if ($this->order){
278
                    if($this->order->exists()) {
279
                        $this->order->calculateOrderAttributes($force = false);
280
                    }
281
                    if (! $this->order->SessionID) {
282
                        $this->order->write();
283
                    }
284
                    //add session ID...
285
                }
286
            }
287
            //try it again
288
            //but limit to three, just in case ...
289
            //just in case ...
290
            if (!$this->order && $recurseCount < 3) {
291
                ++$recurseCount;
292
293
                return $this->currentOrder($recurseCount, $order);
294
            }
295
296
            return $this->order;
297
        } else {
298
299
            //we still return an order so that we do not end up with errors...
300
            return Order::create();
301
        }
302
    }
303
304
305
    private static $_allow_writes_cache = null;
306
307
    /**
308
     * can the current user use sessions and therefore write to cart???
309
     * the method also returns if an order has explicitely been set
310
     * @return Boolean
311
     */
312
    protected function allowWrites()
313
    {
314
        if(self::$_allow_writes_cache === null) {
315
            if($this->order) {
316
                self::$_allow_writes_cache = true;
317
            } else {
318
                if ( php_sapi_name() !== 'cli' ) {
319
                    if ( version_compare(phpversion(), '5.4.0', '>=') ) {
320
                        self::$_allow_writes_cache = session_status() === PHP_SESSION_ACTIVE ? true : false;
321
                    } else {
322
                        self::$_allow_writes_cache = session_id() === '' ? true : false;
323
                    }
324
                } else {
325
                    self::$_allow_writes_cache = false;
326
                }
327
            }
328
        }
329
330
        return self::$_allow_writes_cache;
331
332
    }
333
334
    /**
335
     * Allows access to the current order from anywhere in the code..
336
     *
337
     * @param Order $order (optional)
0 ignored issues
show
Should the type for parameter $order not be Order|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
338
     *
339
     * @return ShoppingCart Object
340
     */
341
    public function Link($order = null)
342
    {
343
        $order = self::singleton()->currentOrder(0, $order = null);
344
        if ($order) {
345
            return $order->Link();
346
        }
347
    }
348
349
    /**
350
     * Adds any number of items to the cart.
351
     * Returns the order item on succes OR false on failure.
352
     *
353
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
354
     * @param float      $quantity   - number of items add.
0 ignored issues
show
Should the type for parameter $quantity not be integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
355
     * @param mixed      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
356
     *                                 if you make it a form, it will save the form into the orderitem
357
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
358
     *
359
     * @return false | DataObject (OrderItem)
0 ignored issues
show
Should the return type not be false|OrderItem|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
360
     */
361
    public function addBuyable(BuyableModel $buyable, $quantity = 1, $parameters = array())
362
    {
363
        if($this->allowWrites()) {
364
            if (!$buyable) {
365
                $this->addMessage(_t('Order.ITEMCOULDNOTBEFOUND', 'This item could not be found.'), 'bad');
366
                return false;
367
            }
368
            if (!$buyable->canPurchase()) {
369
                $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'This item is not for sale.'), 'bad');
370
                return false;
371
            }
372
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
373
            $quantity = $this->prepareQuantity($buyable, $quantity);
374
            if ($item && $quantity) { //find existing order item or make one
375
                $item->Quantity += $quantity;
0 ignored issues
show
The property Quantity does not exist on object<OrderItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
376
                $item->write();
377
                $this->currentOrder()->Attributes()->add($item); //save to current order
378
                //TODO: distinquish between incremented and set
379
                //TODO: use sprintf to allow product name etc to be included in message
380
                if ($quantity > 1) {
381
                    $msg = _t('Order.ITEMSADDED', 'Items added.');
382
                } else {
383
                    $msg = _t('Order.ITEMADDED', 'Item added.');
384
                }
385
                $this->addMessage($msg, 'good');
386
            } elseif (!$item) {
387
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
388
            } else {
389
                $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'Item could not be added.'), 'bad');
390
            }
391
392
            return $item;
393
        }
394
    }
395
396
    /**
397
     * Sets quantity for an item in the cart.
398
     *
399
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
400
     *
401
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
402
     * @param float      $quantity   - number of items add.
403
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
404
     *
405
     * @return false | DataObject (OrderItem) | null
0 ignored issues
show
Should the return type not be OrderItem|false|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
406
     */
407
    public function setQuantity(BuyableModel $buyable, $quantity, array $parameters = array())
408
    {
409
        if($this->allowWrites()) {
410
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
411
            $quantity = $this->prepareQuantity($buyable, $quantity);
412
            if ($item) {
413
                $item->Quantity = $quantity; //remove quantity
0 ignored issues
show
The property Quantity does not exist on object<OrderItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
414
                $item->write();
415
                $this->addMessage(_t('Order.ITEMUPDATED', 'Item updated.'), 'good');
416
417
                return $item;
418
            } else {
419
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
420
            }
421
422
            return false;
423
        }
424
    }
425
426
    /**
427
     * Removes any number of items from the cart.
428
     *
429
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
430
     *
431
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
432
     * @param float      $quantity   - number of items add.
0 ignored issues
show
Should the type for parameter $quantity not be integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
433
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
434
     *
435
     * @return false | OrderItem | null
0 ignored issues
show
Should the return type not be OrderItem|false|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
436
     */
437
    public function decrementBuyable(BuyableModel $buyable, $quantity = 1, array $parameters = array())
438
    {
439
        if($this->allowWrites()) {
440
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
441
            $quantity = $this->prepareQuantity($buyable, $quantity);
442
            if ($item) {
443
                $item->Quantity -= $quantity; //remove quantity
0 ignored issues
show
The property Quantity does not exist on object<OrderItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
444
                if ($item->Quantity < 0) {
0 ignored issues
show
The property Quantity does not exist on object<OrderItem>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
445
                    $item->Quantity = 0;
0 ignored issues
show
The property Quantity does not exist on object<OrderItem>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
446
                }
447
                $item->write();
448
                if ($quantity > 1) {
449
                    $msg = _t('Order.ITEMSREMOVED', 'Items removed.');
450
                } else {
451
                    $msg = _t('Order.ITEMREMOVED', 'Item removed.');
452
                }
453
                $this->addMessage($msg, 'good');
454
455
                return $item;
456
            } else {
457
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
458
            }
459
460
            return false;
461
        }
462
463
    }
464
465
    /**
466
     * Delete item from the cart.
467
     *
468
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
469
     *
470
     * @param OrderItem $buyable    - the buyable (generally a product) being added to the cart
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
471
     * @param array     $parameters - array of parameters to target a specific order item. eg: group=1, length=5
472
     *
473
     * @return bool | item | null - successfully removed
0 ignored issues
show
Should the return type not be OrderItem|false|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
474
     */
475
    public function deleteBuyable(BuyableModel $buyable, array $parameters = array())
476
    {
477
        if($this->allowWrites()) {
478
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = true);
479
            if ($item) {
480
                $this->currentOrder()->Attributes()->remove($item);
481
                $item->delete();
482
                $item->destroy();
483
                $this->addMessage(_t('Order.ITEMCOMPLETELYREMOVED', 'Item removed from cart.'), 'good');
484
485
                return $item;
486
            } else {
487
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
488
489
                return false;
490
            }
491
        }
492
    }
493
494
    /**
495
     * Checks and prepares variables for a quantity change (add, edit, remove) for an Order Item.
496
     *
497
     * @param DataObject    $buyable             - the buyable (generally a product) being added to the cart
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
498
     * @param float         $quantity            - number of items add.
499
     * @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.
0 ignored issues
show
There is no parameter named $mustBeExistingItems. Did you maybe mean $mustBeExistingItem?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
500
     * @param array | Form  $parameters          - array of parameters to target a specific order item. eg: group=1, length=5*
501
     *                                           - form saved into item...
502
     *
503
     * @return bool | DataObject ($orderItem)
0 ignored issues
show
Should the return type not be false|OrderItem?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
504
     */
505
    protected function prepareOrderItem(BuyableModel $buyable, $parameters = array(), $mustBeExistingItem = true)
506
    {
507
        $parametersArray = $parameters;
508
        $form = null;
509
        if ($parameters instanceof Form) {
510
            $parametersArray = array();
511
            $form = $parameters;
512
        }
513
        if (!$buyable) {
514
            user_error('No buyable was provided', E_USER_WARNING);
515
        }
516
        if (!$buyable->canPurchase()) {
517
            $item = $this->getExistingItem($buyable, $parametersArray);
518
            if ($item && $item->exists()) {
519
                $item->delete();
520
                $item->destroy();
521
            }
522
523
            return false;
524
        }
525
        $item = null;
526
        if ($mustBeExistingItem) {
527
            $item = $this->getExistingItem($buyable, $parametersArray);
528
        } else {
529
            $item = $this->findOrMakeItem($buyable, $parametersArray); //find existing order item or make one
530
        }
531
        if (!$item) {
532
            //check for existence of item
533
            return false;
534
        }
535
        if ($form) {
536
            $form->saveInto($item);
537
        }
538
539
        return $item;
540
    }
541
542
    /**
543
     * @todo: what does this method do???
544
     *
545
     * @return int
0 ignored issues
show
Should the return type not be false|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
546
     *
547
     * @param DataObject ($buyable)
548
     * @param float $quantity
549
     */
550
    protected function prepareQuantity(BuyableModel $buyable, $quantity)
551
    {
552
        $quantity = round($quantity, $buyable->QuantityDecimals());
553
        if ($quantity < 0 || (!$quantity && $quantity !== 0)) {
554
            $this->addMessage(_t('Order.INVALIDQUANTITY', 'Invalid quantity.'), 'warning');
555
556
            return false;
557
        }
558
559
        return $quantity;
560
    }
561
562
    /**
563
     * Helper function for making / retrieving order items.
564
     * we do not need things like "canPurchase" here, because that is with the "addBuyable" method.
565
     * NOTE: does not write!
566
     *
567
     * @param DataObject $buyable
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
568
     * @param array      $parameters
569
     *
570
     * @return OrderItem
571
     */
572
    public function findOrMakeItem(BuyableModel $buyable, array $parameters = array())
573
    {
574
        if($this->allowWrites()) {
575
            if ($item = $this->getExistingItem($buyable, $parameters)) {
576
                //do nothing
577
            } else {
578
                //otherwise create a new item
579
                if (!($buyable instanceof BuyableModel)) {
580
                    $this->addMessage(_t('ShoppingCart.ITEMNOTFOUND', 'Item is not buyable.'), 'bad');
581
582
                    return false;
583
                }
584
                $className = $buyable->classNameForOrderItem();
585
                $item = new $className();
586
                if ($order = $this->currentOrder()) {
587
                    $item->OrderID = $order->ID;
588
                    $item->BuyableID = $buyable->ID;
589
                    $item->BuyableClassName = $buyable->ClassName;
590
                    if (isset($buyable->Version)) {
591
                        $item->Version = $buyable->Version;
592
                    }
593
                }
594
            }
595
            if ($parameters) {
596
                $item->Parameters = $parameters;
597
            }
598
599
            return $item;
600
        } else {
601
            return OrderItem::create();
602
        }
603
    }
604
605
    /**
606
     * submit the order so that it is no longer available
607
     * in the cart but will continue its journey through the
608
     * order steps.
609
     *
610
     * @return bool
611
     */
612
    public function submit()
613
    {
614
        if($this->allowWrites()) {
615
            $this->currentOrder()->tryToFinaliseOrder();
616
            $this->clear();
617
            //little hack to clear static memory
618
            OrderItem::reset_price_has_been_fixed($this->currentOrder()->ID);
619
620
            return true;
621
        }
622
623
        return false;
624
    }
625
626
    /**
627
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
628
     *
629
     * @return bool | null
0 ignored issues
show
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
630
     */
631
    public function save()
632
    {
633
        if($this->allowWrites()) {
634
            $this->currentOrder()->write();
635
            $this->addMessage(_t('Order.ORDERSAVED', 'Order Saved.'), 'good');
636
637
            return true;
638
        }
639
    }
640
641
    /**
642
     * Clears the cart contents completely by removing the orderID from session, and
643
     * thus creating a new cart on next request.
644
     *
645
     * @return bool
646
     */
647
    public function clear()
648
    {
649
        //we keep this here so that a flush can be added...
650
        set_time_limit(1 * 60);
651
        self::$_singletoncart = null;
652
        $this->order = null;
653
        $this->messages = array();
654
        foreach (self::$session_variable_names as $name) {
655
            $sessionVariableName = $this->sessionVariableName($name);
656
            Session::set($sessionVariableName, null);
657
            Session::clear($sessionVariableName);
658
            Session::save();
659
        }
660
        $memberID = Intval(Member::currentUserID());
661
        if ($memberID) {
662
            $orders = Order::get()->filter(array('MemberID' => $memberID));
663
            if ($orders && $orders->count()) {
664
                foreach ($orders as $order) {
665
                    if (! $order->IsSubmitted()) {
666
                        $order->delete();
667
                    }
668
                }
669
            }
670
        }
671
672
        return true;
673
    }
674
675
    /**
676
     * alias for clear.
677
     */
678
    public function reset()
679
    {
680
        return $this->clear();
681
    }
682
683
    /**
684
     * Removes a modifier from the cart
685
     * It does not actually remove it, but it just
686
     * sets it as "removed", to avoid that it is being
687
     * added again.
688
     *
689
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
690
     *
691
     * @param OrderModifier $modifier
692
     *
693
     * @return bool | null
0 ignored issues
show
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
694
     */
695
    public function removeModifier(OrderModifier $modifier)
696
    {
697
        if($this->allowWrites()) {
698
            $modifier = (is_numeric($modifier)) ? OrderModifier::get()->byID($modifier) : $modifier;
699
            if (!$modifier) {
700
                $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
701
702
                return false;
703
            }
704
            if (!$modifier->CanBeRemoved()) {
705
                $this->addMessage(_t('Order.MODIFIERCANNOTBEREMOVED', 'Modifier can not be removed.'), 'bad');
706
707
                return false;
708
            }
709
            $modifier->HasBeenRemoved = 1;
710
            $modifier->onBeforeRemove();
711
            $modifier->write();
712
            $modifier->onAfterRemove();
713
            $this->addMessage(_t('Order.MODIFIERREMOVED', 'Removed.'), 'good');
714
715
            return true;
716
        }
717
    }
718
719
    /**
720
     * Removes a modifier from the cart.
721
     *
722
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
723
     *
724
     * @param Int/ OrderModifier
725
     *
726
     * @return bool
0 ignored issues
show
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
727
     */
728
    public function addModifier($modifier)
729
    {
730
        if($this->allowWrites()) {
731
            if (is_numeric($modifier)) {
732
                $modifier = OrderModifier::get()->byID($modifier);
733
            } elseif (!(is_a($modifier, Object::getCustomClass('OrderModifier')))) {
734
                user_error('Bad parameter provided to ShoppingCart::addModifier', E_USER_WARNING);
735
            }
736
            if (!$modifier) {
737
                $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
738
739
                return false;
740
            }
741
            $modifier->HasBeenRemoved = 0;
742
            $modifier->write();
743
            $this->addMessage(_t('Order.MODIFIERREMOVED', 'Added.'), 'good');
744
745
            return true;
746
        }
747
    }
748
749
    /**
750
     * Sets an order as the current order.
751
     *
752
     * @param int | Order $order
753
     *
754
     * @return bool
755
     */
756
    public function loadOrder($order)
757
    {
758
        if($this->allowWrites()) {
759
            //TODO: how to handle existing order
760
            //TODO: permission check - does this belong to another member? ...or should permission be assumed already?
761
            if (is_numeric($order)) {
762
                $this->order = Order::get()->byID($order);
763
            } elseif (is_a($order, Object::getCustomClass('Order'))) {
764
                $this->order = $order;
765
            } else {
766
                user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
767
            }
768
            if ($this->order) {
769
                //first can view and then, if can view, set as session...
770
                if ($this->order->canView()) {
771
                    $this->order->init(true);
772
                    $sessionVariableName = $this->sessionVariableName('OrderID');
773
                    //we set session ID after can view check ...
774
                    Session::set($sessionVariableName, $this->order->ID);
775
                    $this->addMessage(_t('Order.LOADEDEXISTING', 'Order loaded.'), 'good');
776
777
                    return true;
778
                } else {
779
                    $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
780
781
                    return false;
782
                }
783
            } else {
784
                $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
785
786
                return false;
787
            }
788
        } else {
789
            $this->addMessage(_t('Order.NOSAVE', 'You can not load orders as your session functionality is turned off.'), 'bad');
790
791
            return false;
792
        }
793
794
    }
795
796
    /**
797
     * NOTE: tried to copy part to the Order Class - but that was not much of a go-er.
798
     *
799
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
800
     *
801
     * @param int | Order $order
802
     *
803
     * @return Order | false | null
0 ignored issues
show
Should the return type not be Order|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
804
     **/
805
    public function copyOrder($oldOrder)
806
    {
807
        if($this->allowWrites()) {
808
            if (is_numeric($oldOrder)) {
809
                $oldOrder = Order::get()->byID(intval($oldOrder));
810
            } elseif (is_a($oldOrder, Object::getCustomClass('Order'))) {
811
                //$oldOrder = $oldOrder;
812
            } else {
813
                user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
814
            }
815
            if ($oldOrder) {
816
                if ($oldOrder->canView() && $oldOrder->IsSubmitted()) {
817
                    $newOrder = Order::create();
818
                    //copying fields.
819
                    $newOrder->UseShippingAddress = $oldOrder->UseShippingAddress;
0 ignored issues
show
The property UseShippingAddress does not exist on object<Order>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
820
                    //important to set it this way...
821
                    $newOrder->setCurrency($oldOrder->CurrencyUsed());
822
                    $newOrder->MemberID = $oldOrder->MemberID;
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
823
                    //load the order
824
                    $newOrder->write();
825
                    $this->loadOrder($newOrder);
826
                    $items = OrderItem::get()
827
                        ->filter(array('OrderID' => $oldOrder->ID));
828
                    if ($items->count()) {
829
                        foreach ($items as $item) {
830
                            $buyable = $item->Buyable($current = true);
831
                            if ($buyable->canPurchase()) {
832
                                $this->addBuyable($buyable, $item->Quantity);
833
                            }
834
                        }
835
                    }
836
                    $newOrder->CreateOrReturnExistingAddress('BillingAddress');
837
                    $newOrder->CreateOrReturnExistingAddress('ShippingAddress');
838
                    $newOrder->write();
839
                    $this->addMessage(_t('Order.ORDERCOPIED', 'Order has been copied.'), 'good');
840
841
                    return $newOrder;
842
                } else {
843
                    $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
844
845
                    return false;
846
                }
847
            } else {
848
                $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
849
850
                return false;
851
            }
852
        }
853
    }
854
855
    /**
856
     * sets country in order so that modifiers can be recalculated, etc...
857
     *
858
     * @param string - $countryCode
859
     *
860
     * @return bool
0 ignored issues
show
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
861
     **/
862
    public function setCountry($countryCode)
863
    {
864
        if($this->allowWrites()) {
865
            if (EcommerceCountry::code_allowed($countryCode)) {
866
                $this->currentOrder()->SetCountryFields($countryCode);
867
                $this->addMessage(_t('Order.UPDATEDCOUNTRY', 'Updated country.'), 'good');
868
869
                return true;
870
            } else {
871
                $this->addMessage(_t('Order.NOTUPDATEDCOUNTRY', 'Could not update country.'), 'bad');
872
873
                return false;
874
            }
875
        }
876
    }
877
878
    /**
879
     * sets region in order so that modifiers can be recalculated, etc...
880
     *
881
     * @param int | String - $regionID you can use the ID or the code.
882
     *
883
     * @return bool
884
     **/
885
    public function setRegion($regionID)
886
    {
887
        if (EcommerceRegion::regionid_allowed($regionID)) {
888
            $this->currentOrder()->SetRegionFields($regionID);
889
            $this->addMessage(_t('ShoppingCart.REGIONUPDATED', 'Region updated.'), 'good');
890
891
            return true;
892
        } else {
893
            $this->addMessage(_t('ORDER.NOTUPDATEDREGION', 'Could not update region.'), 'bad');
894
895
            return false;
896
        }
897
    }
898
899
    /**
900
     * sets the display currency for the cart.
901
     *
902
     * @param string $currencyCode
903
     *
904
     * @return bool
905
     **/
906
    public function setCurrency($currencyCode)
907
    {
908
        $currency = EcommerceCurrency::get_one_from_code($currencyCode);
909
        if ($currency) {
910
            if ($this->currentOrder()->MemberID) {
0 ignored issues
show
The property MemberID does not exist on object<Order>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
911
                $member = $this->currentOrder()->Member();
912
                if ($member && $member->exists()) {
913
                    $member->SetPreferredCurrency($currency);
914
                }
915
            }
916
            $this->currentOrder()->UpdateCurrency($currency);
917
            $msg = _t('Order.CURRENCYUPDATED', 'Currency updated.');
918
            $this->addMessage($msg, 'good');
919
920
            return true;
921
        } else {
922
            $msg = _t('Order.CURRENCYCOULDNOTBEUPDATED', 'Currency could not be updated.');
923
            $this->addMessage($msg, 'bad');
924
925
            return false;
926
        }
927
    }
928
929
    /**
930
     * Produces a debug of the shopping cart.
931
     */
932
    public function debug()
933
    {
934
        if (Director::isDev() || Permission::check('ADMIN')) {
935
            debug::show($this->currentOrder());
936
937
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Country</h1>';
938
            echo 'GEOIP Country: '.EcommerceCountry::get_country_from_ip().'<br />';
939
            echo 'Calculated Country Country: '.EcommerceCountry::get_country().'<br />';
940
941
            echo '<blockquote><blockquote><blockquote><blockquote>';
942
943
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Items</h1>';
944
            $items = $this->currentOrder()->Items();
945
            echo $items->sql();
946
            echo '<hr />';
947
            if ($items->count()) {
948
                foreach ($items as $item) {
949
                    Debug::show($item);
950
                }
951
            } else {
952
                echo '<p>there are no items for this order</p>';
953
            }
954
955
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Modifiers</h1>';
956
            $modifiers = $this->currentOrder()->Modifiers();
957
            if ($modifiers->count()) {
958
                foreach ($modifiers as $modifier) {
959
                    Debug::show($modifier);
960
                }
961
            } else {
962
                echo '<p>there are no modifiers for this order</p>';
963
            }
964
965
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Addresses</h1>';
966
            $billingAddress = $this->currentOrder()->BillingAddress();
967
            if ($billingAddress && $billingAddress->exists()) {
968
                Debug::show($billingAddress);
969
            } else {
970
                echo '<p>there is no billing address for this order</p>';
971
            }
972
            $shippingAddress = $this->currentOrder()->ShippingAddress();
973
            if ($shippingAddress && $shippingAddress->exists()) {
974
                Debug::show($shippingAddress);
975
            } else {
976
                echo '<p>there is no shipping address for this order</p>';
977
            }
978
979
            $currencyUsed = $this->currentOrder()->CurrencyUsed();
0 ignored issues
show
Documentation Bug introduced by
The method CurrencyUsed does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
980
            if ($currencyUsed && $currencyUsed->exists()) {
981
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Currency</h1>';
982
                Debug::show($currencyUsed);
983
            }
984
985
            $cancelledBy = $this->currentOrder()->CancelledBy();
0 ignored issues
show
Documentation Bug introduced by
The method CancelledBy does not exist on object<Order>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
986
            if ($cancelledBy && $cancelledBy->exists()) {
987
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Cancelled By</h1>';
988
                Debug::show($cancelledBy);
989
            }
990
991
            $logs = $this->currentOrder()->OrderStatusLogs();
992
            if ($logs && $logs->count()) {
993
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Logs</h1>';
994
                foreach ($logs as $log) {
995
                    Debug::show($log);
996
                }
997
            }
998
999
            $payments = $this->currentOrder()->Payments();
1000
            if ($payments  && $payments->count()) {
1001
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Payments</h1>';
1002
                foreach ($payments as $payment) {
1003
                    Debug::show($payment);
1004
                }
1005
            }
1006
1007
            $emails = $this->currentOrder()->Emails();
1008
            if ($emails && $emails->count()) {
1009
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Emails</h1>';
1010
                foreach ($emails as $email) {
1011
                    Debug::show($email);
1012
                }
1013
            }
1014
1015
            echo '</blockquote></blockquote></blockquote></blockquote>';
1016
        } else {
1017
            echo 'Please log in as admin first';
1018
        }
1019
    }
1020
1021
    /**
1022
     * Stores a message that can later be returned via ajax or to $form->sessionMessage();.
1023
     *
1024
     * @param $message - the message, which could be a notification of successful action, or reason for failure
1025
     * @param $type - please use good, bad, warning
1026
     */
1027
    public function addMessage($message, $status = 'good')
1028
    {
1029
        //clean status for the lazy programmer
1030
        //TODO: remove the awkward replace
1031
        $status = strtolower($status);
1032
        str_replace(array('success', 'failure'), array('good', 'bad'), $status);
1033
        $statusOptions = array('good', 'bad', 'warning');
1034
        if (!in_array($status, $statusOptions)) {
1035
            user_error('Message status should be one of the following: '.implode(',', $statusOptions), E_USER_NOTICE);
1036
        }
1037
        $this->messages[] = array(
1038
            'Message' => $message,
1039
            'Type' => $status,
1040
        );
1041
    }
1042
1043
    /*******************************************************
1044
    * HELPER FUNCTIONS
1045
    *******************************************************/
1046
1047
    /**
1048
     * Gets an existing order item based on buyable and passed parameters.
1049
     *
1050
     * @param DataObject $buyable
0 ignored issues
show
Should the type for parameter $buyable not be BuyableModel?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1051
     * @param array      $parameters
1052
     *
1053
     * @return OrderItem | null
0 ignored issues
show
Should the return type not be OrderItem|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1054
     */
1055
    protected function getExistingItem(BuyableModel $buyable, array $parameters = array())
1056
    {
1057
        $filterString = $this->parametersToSQL($parameters);
1058
        if ($order = $this->currentOrder()) {
1059
            $orderID = $order->ID;
1060
            $obj = OrderItem::get()
1061
                ->where(
1062
                    " \"BuyableClassName\" = '".$buyable->ClassName."' AND
1063
                    \"BuyableID\" = ".$buyable->ID.' AND
1064
                    "OrderID" = '.$orderID.' '.
1065
                    $filterString
1066
                )
1067
                ->First();
1068
1069
            return $obj;
1070
        }
1071
    }
1072
1073
    /**
1074
     * Removes parameters that aren't in the default array, merges with default parameters, and converts raw2SQL.
1075
     *
1076
     * @param array $parameters
1077
     *
1078
     * @return cleaned array
0 ignored issues
show
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1079
     */
1080
    protected function cleanParameters(array $params = array())
1081
    {
1082
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
1083
        $newarray = array_merge(array(), $defaultParamFilters); //clone array
1084
        if (!count($newarray)) {
1085
            return array(); //no use for this if there are not parameters defined
1086
        }
1087
        foreach ($newarray as $field => $value) {
1088
            if (isset($params[$field])) {
1089
                $newarray[$field] = Convert::raw2sql($params[$field]);
1090
            }
1091
        }
1092
1093
        return $newarray;
1094
    }
1095
1096
    /**
1097
     * @param array $parameters
1098
     *                          Converts parameter array to SQL query filter
1099
     */
1100
    protected function parametersToSQL(array $parameters = array())
1101
    {
1102
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
1103
        if (!count($defaultParamFilters)) {
1104
            return ''; //no use for this if there are not parameters defined
1105
        }
1106
        $cleanedparams = $this->cleanParameters($parameters);
1107
        $outputArray = array();
1108
        foreach ($cleanedparams as $field => $value) {
1109
            $outputarray[$field] = '"'.$field.'" = '.$value;
1110
        }
1111
        if (count($outputArray)) {
1112
            return implode(' AND ', $outputArray);
1113
        }
1114
1115
        return '';
1116
    }
1117
1118
    /*******************************************************
1119
    * UI MESSAGE HANDLING
1120
    *******************************************************/
1121
1122
    /**
1123
     * Retrieves all good, bad, and ugly messages that have been produced during the current request.
1124
     *
1125
     * @return array of messages
1126
     */
1127
    public function getMessages()
1128
    {
1129
        $sessionVariableName = $this->sessionVariableName('Messages');
1130
        //get old messages
1131
        $messages = unserialize(Session::get($sessionVariableName));
1132
        //clear old messages
1133
        Session::clear($sessionVariableName, '');
1134
        //set to form????
1135
        if ($messages && count($messages)) {
1136
            $this->messages = array_merge($messages, $this->messages);
1137
        }
1138
1139
        return $this->messages;
1140
    }
1141
1142
    /**
1143
     *Saves current messages in session for retrieving them later.
1144
     *
1145
     *@return array of messages
0 ignored issues
show
Should the return type not be array|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1146
     */
1147
    protected function StoreMessagesInSession()
1148
    {
1149
        $sessionVariableName = $this->sessionVariableName('Messages');
1150
        Session::set($sessionVariableName, serialize($this->messages));
1151
    }
1152
1153
    /**
1154
     * This method is used to return data after an ajax call was made.
1155
     * When a asynchronious request is made to the shopping cart (ajax),
1156
     * then you will first action the request and then use this function
1157
     * to return some values.
1158
     *
1159
     * It can also be used without ajax, in wich case it will redirects back
1160
     * to the last page.
1161
     *
1162
     * Note that you can set the ajax response class in the configuration file.
1163
     *
1164
     *
1165
     * @param string $message
1166
     * @param string $status
1167
     * @param Form   $form
0 ignored issues
show
Should the type for parameter $form not be null|Form?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1168
     * @returns String (JSON)
1169
     */
1170
    public function setMessageAndReturn($message = '', $status = '', Form $form = null)
0 ignored issues
show
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1171
    {
1172
        if ($message && $status) {
1173
            $this->addMessage($message, $status);
1174
        }
1175
        //TODO: handle passing back multiple messages
1176
1177
        if (Director::is_ajax()) {
1178
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
1179
            $obj = new $responseClass();
1180
1181
            return $obj->ReturnCartData($this->getMessages());
1182
        } else {
1183
            //TODO: handle passing a message back to a form->sessionMessage
1184
            $this->StoreMessagesInSession();
1185
            if ($form) {
1186
                //lets make sure that there is an order
1187
                $this->currentOrder();
1188
                //nowe we can (re)calculate the order
1189
                $this->order->calculateOrderAttributes($force = false);
1190
                $form->sessionMessage($message, $status);
1191
                //let the form controller do the redirectback or whatever else is needed.
1192
            } else {
1193
                if (empty($_REQUEST['BackURL']) && Controller::has_curr()) {
1194
                    Controller::curr()->redirectBack();
1195
                } else {
1196
                    Controller::curr()->redirect(urldecode($_REQUEST['BackURL']));
1197
                }
1198
            }
1199
1200
            return;
1201
        }
1202
    }
1203
1204
    /**
1205
     * @return EcommerceDBConfig
1206
     */
1207
    protected function EcomConfig()
1208
    {
1209
        return EcommerceDBConfig::current_ecommerce_db_config();
1210
    }
1211
1212
    /**
1213
     * Return the name of the session variable that should be used.
1214
     *
1215
     * @param string $name
1216
     *
1217
     * @return string
1218
     */
1219
    protected function sessionVariableName($name = '')
1220
    {
1221
        if (!in_array($name, self::$session_variable_names)) {
1222
            user_error("Tried to set session variable $name, that is not in use", E_USER_NOTICE);
1223
        }
1224
        $sessionCode = EcommerceConfig::get('ShoppingCart', 'session_code');
1225
1226
        return $sessionCode.'_'.$name;
1227
    }
1228
}
1229