Completed
Push — master ( e26ad5...3cf601 )
by Roman
18:41 queued 06:38
created

ShoppingCart::message()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
crap 1
1
<?php
2
3
/**
4
 * Encapsulated manipulation of the current order using a singleton pattern.
5
 *
6
 * Ensures that an order is only started (persisted to DB) when necessary,
7
 * and all future changes are on the same order, until the order has is placed.
8
 * The requirement for starting an order is to adding an item to the cart.
9
 *
10
 * @package shop
11
 */
12
class ShoppingCart extends Object
0 ignored issues
show
Complexity introduced by
This class has a complexity of 73 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
13
{
14
    private static $cartid_session_name = 'shoppingcartid';
15
16
    private $order;
17
18
    private $calculateonce = false;
19
20
    private $message;
21
22
    private $type;
23
24
    /**
25
     * Access for only allowing access to one (singleton) ShoppingCart.
26
     *
27
     * @return ShoppingCart
28
     */
29 108
    public static function singleton()
30
    {
31 108
        return Injector::inst()->get('ShoppingCart');
32
    }
33
34
    /**
35
     * Shortened alias for ShoppingCart::singleton()->current()
36
     *
37
     * @return Order
38
     */
39 17
    public static function curr()
40
    {
41 17
        return self::singleton()->current();
42
    }
43
44
    /**
45
     * Get the current order, or return null if it doesn't exist.
46
     *
47
     * @return Order
48
     */
49 108
    public function current()
50
    {
51
        //find order by id saved to session (allows logging out and retaining cart contents)
52 108
        if (!$this->order && $sessionid = Session::get(self::config()->cartid_session_name)) {
53
            $this->order = Order::get()->filter(
54
                array(
55
                    "Status" => "Cart",
56
                    "ID" => $sessionid,
57
                )
58
            )->first();
59 4
        }
60 108
        if (!$this->calculateonce && $this->order) {
61 16
            $this->order->calculate();
62 16
            $this->calculateonce = true;
63 16
        }
64
65 108
        return $this->order ? $this->order : false;
66
    }
67
68
    /**
69
     * Set the current cart
70
     *
71
     * @param Order
72
     *
73
     * @return ShoppingCart
74
     */
75 30
    public function setCurrent(Order $cart)
76
    {
77 7
        if (!$cart->IsCart()) {
78
            trigger_error("Passed Order object is not cart status", E_ERROR);
79
        }
80 7
        $this->order = $cart;
81 7
        Session::set(self::config()->cartid_session_name, $cart->ID);
82
83 30
        return $this;
84
    }
85
86
    /**
87
     * Helper that only allows orders to be started internally.
88
     *
89
     * @return Order
90
     */
91 23
    protected function findOrMake()
92
    {
93 23
        if ($this->current()) {
94 23
            return $this->current();
95
        }
96 21
        $this->order = Order::create();
97 21
        if (Member::config()->login_joins_cart && Member::currentUserID()) {
98 12
            $this->order->MemberID = Member::currentUserID();
99 12
        }
100 21
        $this->order->write();
101 21
        $this->order->extend('onStartOrder');
102 21
        Session::set(self::config()->cartid_session_name, $this->order->ID);
103
104 21
        return $this->order;
105
    }
106
107
    /**
108
     * Adds an item to the cart
109
     *
110
     * @param Buyable $buyable
111
     * @param int $quantity
112
     * @param array $filter
113
     *
114
     * @return boolean|OrderItem false or the new/existing item
115
     */
116 22
    public function add(Buyable $buyable, $quantity = 1, $filter = array())
117
    {
118 22
        $order = $this->findOrMake();
119
120
        // If an extension throws an exception, error out
121
        try {
122 22
            $order->extend("beforeAdd", $buyable, $quantity, $filter);
123 22
        } catch (Exception $exception){
124
            return $this->error($exception->getMessage());
125
        }
126
127 22
        if (!$buyable) {
128
            return $this->error(_t("ShoppingCart.ProductNotFound", "Product not found."));
129
        }
130
131 22
        $item = $this->findOrMakeItem($buyable, $quantity, $filter);
132 22
        if (!$item) {
133 1
            return false;
134
        }
135 22
        if (!$item->_brandnew) {
0 ignored issues
show
Documentation introduced by
The property _brandnew 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...
136 5
            $item->Quantity += $quantity;
0 ignored issues
show
Documentation introduced by
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...
137 5
        } else {
138 22
            $item->Quantity = $quantity;
0 ignored issues
show
Documentation introduced by
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...
139
        }
140
141
        // If an extension throws an exception, error out
142
        try {
143 22
            $order->extend("afterAdd", $item, $buyable, $quantity, $filter);
144 22
        } catch (Exception $exception){
145 1
            return $this->error($exception->getMessage());
146
        }
147
148 22
        $item->write();
149 22
        $this->message(_t("ShoppingCart.ItemAdded", "Item has been added successfully."));
150
151 22
        return $item;
152
    }
153
154
    /**
155
     * Remove an item from the cart.
156
     *
157
     * @param Buyable $buyable
158
     * @param int $quantity - number of items to remove, or leave null for all items (default)
159
     * @param array $filter
160
     *
161
     * @return boolean success/failure
162
     */
163 4
    public function remove(Buyable $buyable, $quantity = null, $filter = array())
164
    {
165 4
        $order = $this->current();
166
167 4
        if (!$order) {
168
            return $this->error(_t("ShoppingCart.NoOrder", "No current order."));
169
        }
170
171
        // If an extension throws an exception, error out
172
        try {
173 4
            $order->extend("beforeRemove", $buyable, $quantity, $filter);
174 4
        } catch (Exception $exception){
175
            return $this->error($exception->getMessage());
176
        }
177
178 4
        $item = $this->get($buyable, $filter);
179
180 4
        if (!$item || !$this->removeOrderItem($item, $quantity)) {
181 1
            return false;
182
        }
183
184
        // If an extension throws an exception, error out
185
        // TODO: There should be a rollback
186
        try {
187 4
            $order->extend("afterRemove", $item, $buyable, $quantity, $filter);
188 4
        } catch (Exception $exception){
189
            return $this->error($exception->getMessage());
190
        }
191
192 4
        $this->message(_t("ShoppingCart.ItemRemoved", "Item has been successfully removed."));
193
194 4
        return true;
195
    }
196
197
    /**
198
     * Remove a specific order item from cart
199
     * @param OrderItem $item
200
     * @param int $quantity - number of items to remove or leave `null` to remove all items (default)
201
     * @return boolean success/failure
202
     */
203 4
    public function removeOrderItem(OrderItem $item, $quantity = null)
204
    {
205 4
        $order = $this->current();
206
207 4
        if (!$order) {
208
            return $this->error(_t("ShoppingCart.NoOrder", "No current order."));
209
        }
210
211 4
        if (!$item || $item->OrderID != $order->ID) {
0 ignored issues
show
Documentation introduced by
The property OrderID 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...
212
            return $this->error(_t("ShoppingCart.ItemNotFound", "Item not found."));
213
        }
214
215
        //if $quantity will become 0, then remove all
216 4
        if (!$quantity || ($item->Quantity - $quantity) <= 0) {
0 ignored issues
show
Documentation introduced by
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...
Bug Best Practice introduced by
The expression $quantity of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
217 4
            $item->delete();
218 4
            $item->destroy();
219 4
        } else {
220 1
            $item->Quantity -= $quantity;
0 ignored issues
show
Documentation introduced by
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...
221 1
            $item->write();
222
        }
223
224 4
        return true;
225
    }
226
227
    /**
228
     * Sets the quantity of an item in the cart.
229
     * Will automatically add or remove item, if necessary.
230
     *
231
     * @param Buyable $buyable
232
     * @param int $quantity
233
     * @param array $filter
234
     *
235
     * @return boolean|OrderItem false or the new/existing item
236
     */
237 4
    public function setQuantity(Buyable $buyable, $quantity = 1, $filter = array())
238
    {
239 4
        if ($quantity <= 0) {
240
            return $this->remove($buyable, $quantity, $filter);
241
        }
242 4
        $order = $this->findOrMake();
0 ignored issues
show
Unused Code introduced by
$order is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
243 4
        $item = $this->findOrMakeItem($buyable, $quantity, $filter);
244
245 4
        if (!$item || !$this->updateOrderItemQuantity($item, $quantity, $filter)) {
246 1
            return false;
247
        }
248
249 4
        return $item;
250
    }
251
252
    /**
253
     * Update quantity of a given order item
254
     * @param OrderItem $item
255
     * @param int $quantity the new quantity to use
256
     * @param array $filter
257
     * @return boolean success/failure
258
     */
259 19
    public function updateOrderItemQuantity(OrderItem $item, $quantity = 1, $filter = array())
260
    {
261 4
        $order = $this->current();
262
263 4
        if (!$order) {
264 4
            return $this->error(_t("ShoppingCart.NoOrder", "No current order."));
265
        }
266
267 8
        if (!$item || $item->OrderID != $order->ID) {
0 ignored issues
show
Documentation introduced by
The property OrderID 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...
268
            return $this->error(_t("ShoppingCart.ItemNotFound", "Item not found."));
269
        }
270
271 4
        $buyable = $item->Buyable();
272
        // If an extension throws an exception, error out
273 4
        try {
274 4
            $order->extend("beforeSetQuantity", $buyable, $quantity, $filter);
275 4
        } catch (Exception $exception){
276 1
            return $this->error($exception->getMessage());
277
        }
278
279 4
        $item->Quantity = $quantity;
0 ignored issues
show
Documentation introduced by
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...
280
281
        // If an extension throws an exception, error out
282 19
        try {
283 4
            $order->extend("afterSetQuantity", $item, $buyable, $quantity, $filter);
284 4
        } catch (Exception $exception){
285
            return $this->error($exception->getMessage());
286
        }
287
288 4
        $item->write();
289 4
        $this->message(_t("ShoppingCart.QuantitySet", "Quantity has been set."));
290
291 4
        return true;
292
    }
293
294
    /**
295
     * Finds or makes an order item for a given product + filter.
296
     *
297
     * @param Buyable $buyable the buyable
298
     * @param int $quantity quantity to add
299
     * @param array $filter
300
     *
301
     * @return OrderItem the found or created item
302
     */
303 25
    private function findOrMakeItem(Buyable $buyable, $quantity = 1, $filter = array())
304
    {
305 23
        $order = $this->findOrMake();
306
307 23
        if (!$buyable || !$order) {
308
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by ShoppingCart::findOrMakeItem of type OrderItem.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
309
        }
310
311 23
        $item = $this->get($buyable, $filter);
312
313 23
        if (!$item) {
314 23
            $member = Member::currentUser();
315
316 23
            $buyable = $this->getCorrectBuyable($buyable);
317
318 23
            if (!$buyable->canPurchase($member, $quantity)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 314 can also be of type object<DataObject>; however, Buyable::canPurchase() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
319 1
                return $this->error(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->error(_t('...18n_singular_name()))); (boolean) is incompatible with the return type documented by ShoppingCart::findOrMakeItem of type OrderItem.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
320 1
                    _t(
321 1
                        'ShoppingCart.CannotPurchase',
322 1
                        'This {Title} cannot be purchased.',
323 1
                        '',
324 1
                        array('Title' => $buyable->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('Title' => $buyable->i18n_singular_name()) is of type array<string,?,{"Title":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
325 1
                    )
326 1
                );
327
                //TODO: produce a more specific message
328
            }
329
330 23
            $item = $buyable->createItem($quantity, $filter);
331 23
            $item->OrderID = $order->ID;
0 ignored issues
show
Documentation introduced by
The property OrderID 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...
332 23
            $item->write();
333
334 23
            $order->Items()->add($item);
0 ignored issues
show
Documentation Bug introduced by
The method Items 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...
335
336 23
            $item->_brandnew = true; // flag as being new
0 ignored issues
show
Documentation introduced by
The property _brandnew 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...
337 23
        }
338
339 23
        return $item;
340 24
    }
341
342
    /**
343
     * Finds an existing order item.
344
     *
345
     * @param Buyable $buyable
346
     * @param array $customfilter
347
     *
348
     * @return OrderItem the item requested, or false
349
     */
350 28
    public function get(Buyable $buyable, $customfilter = array())
351
    {
352 28
        $order = $this->current();
353 28
        if (!$buyable || !$order) {
354 4
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by ShoppingCart::get of type OrderItem.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
355
        }
356
357 26
        $buyable = $this->getCorrectBuyable($buyable);
358
359
        $filter = array(
360 26
            'OrderID' => $order->ID,
361 26
        );
362 26
        $itemclass = Config::inst()->get(get_class($buyable), 'order_item');
363 26
        $relationship = Config::inst()->get($itemclass, 'buyable_relationship');
364 26
        $filter[$relationship . "ID"] = $buyable->ID;
0 ignored issues
show
Bug introduced by
Accessing ID on the interface Buyable suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
365 26
        $required = array('Order', $relationship);
366 26
        if (is_array($itemclass::config()->required_fields)) {
367 26
            $required = array_merge($required, $itemclass::config()->required_fields);
368 26
        }
369 26
        $query = new MatchObjectFilter($itemclass, array_merge($customfilter, $filter), $required);
370 26
        $item = $itemclass::get()->where($query->getFilter())->first();
371 26
        if (!$item) {
372 26
            return $this->error(_t("ShoppingCart.ItemNotFound", "Item not found."));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->error(_t('...', 'Item not found.')); (boolean) is incompatible with the return type documented by ShoppingCart::get of type OrderItem.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
373
        }
374
375 13
        return $item;
376
    }
377
378
    /**
379
     * Ensure the proper buyable will be returned for a given buyable…
380
     * This is being used to ensure a product with variations cannot be added to the cart…
381
     * a Variation has to be added instead!
382
     * @param Buyable $buyable
383
     * @return Buyable
384
     */
385 26
    public function getCorrectBuyable(Buyable $buyable)
386
    {
387
        if (
388 26
            $buyable instanceof Product &&
389 26
            $buyable->hasExtension('ProductVariationsExtension') &&
390 23
            $buyable->Variations()->count() > 0
391 26
        ) {
392 1
            foreach ($buyable->Variations() as $variation) {
393 1
                if ($variation->canPurchase()) {
394 1
                    return $variation;
395
                }
396
            }
397
        }
398
399 25
        return $buyable;
400
    }
401
402
    /**
403
     * Store old cart id in session order history
404
     * @param int|null $requestedOrderId optional parameter that denotes the order that was requested
405
     */
406
    public function archiveorderid($requestedOrderId = null)
407
    {
408
        $sessionId = Session::get(self::config()->cartid_session_name);
409
        $order = Order::get()
410
            ->filter("Status:not", "Cart")
411
            ->byId($sessionId);
412
        if ($order && !$order->IsCart()) {
413
            OrderManipulation::add_session_order($order);
0 ignored issues
show
Compatibility introduced by
$order of type object<DataObject> is not a sub-type of object<Order>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
414
        }
415
        // in case there was no order requested
416
        // OR there was an order requested AND it's the same one as currently in the session,
417
        // then clear the cart. This check is here to prevent clearing of the cart if the user just
418
        // wants to view an old order (via AccountPage).
419
        if (!$requestedOrderId || ($sessionId == $requestedOrderId)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $requestedOrderId of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
420
            $this->clear();
421
        }
422
    }
423
424
    /**
425
     * Empty / abandon the entire cart.
426
     *
427
     * @param bool $write whether or not to write the abandoned order
428
     * @return bool - true if successful, false if no cart found
429
     */
430 19
    public function clear($write = true)
431
    {
432 19
        Session::clear(self::config()->cartid_session_name);
433 19
        $order = $this->current();
434 19
        $this->order = null;
435 19
        if (!$order) {
436 13
            return $this->error(_t("ShoppingCart.NoCartFound", "No cart found."));
437
        }
438 11
        if ($write) {
439 6
            $order->write();
440 6
        }
441 11
        $this->message(_t("ShoppingCart.Cleared", "Cart was successfully cleared."));
442
443 11
        return true;
444
    }
445
446
    /**
447
     * Store a new error.
448
     */
449 27
    protected function error($message)
450
    {
451 27
        $this->message($message, "bad");
452
453 27
        return false;
454
    }
455
456
    /**
457
     * Store a message to be fed back to user.
458
     *
459
     * @param string $message
460
     * @param string $type - good, bad, warning
461
     */
462 28
    protected function message($message, $type = "good")
463
    {
464 28
        $this->message = $message;
465 28
        $this->type = $type;
466 28
    }
467
468
    public function getMessage()
469
    {
470
        return $this->message;
471
    }
472
473
    public function getMessageType()
474
    {
475
        return $this->type;
476
    }
477
478
    public function clearMessage()
479
    {
480
        $this->message = null;
481
    }
482
483
    //singleton protection
484
    public function __clone()
485
    {
486
        trigger_error('Clone is not allowed.', E_USER_ERROR);
487
    }
488
489
    public function __wakeup()
490
    {
491
        trigger_error('Unserializing is not allowed.', E_USER_ERROR);
492
    }
493
}
494
495
/**
496
 * Manipulate the cart via urls.
497
 */
498
class ShoppingCart_Controller extends Controller
0 ignored issues
show
Complexity introduced by
This class has a complexity of 52 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

Loading history...
499
{
500
    private static $url_segment         = "shoppingcart";
501
502
    private static $direct_to_cart_page = false;
503
504
    protected      $cart;
505
506
    private static $url_handlers        = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
507
        '$Action/$Buyable/$ID' => 'handleAction',
508
    );
509
510
    private static $allowed_actions     = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
511
        'add',
512
        'additem',
513
        'remove',
514
        'removeitem',
515
        'removeall',
516
        'removeallitem',
517
        'setquantity',
518
        'setquantityitem',
519
        'clear',
520
        'debug',
521
    );
522
523 6
    public static function add_item_link(Buyable $buyable, $parameters = array())
524
    {
525 6
        return self::build_url("add", $buyable, $parameters);
526
    }
527
528 2
    public static function remove_item_link(Buyable $buyable, $parameters = array())
529
    {
530 2
        return self::build_url("remove", $buyable, $parameters);
531
    }
532
533 2
    public static function remove_all_item_link(Buyable $buyable, $parameters = array())
534
    {
535 2
        return self::build_url("removeall", $buyable, $parameters);
536
    }
537
538 3
    public static function set_quantity_item_link(Buyable $buyable, $parameters = array())
539
    {
540 3
        return self::build_url("setquantity", $buyable, $parameters);
541
    }
542
543
    /**
544
     * Helper for creating a url
545
     */
546 6
    protected static function build_url($action, $buyable, $params = array())
547
    {
548 6
        if (!$action || !$buyable) {
549
            return false;
550
        }
551 6
        if (SecurityToken::is_enabled() && !self::config()->disable_security_token) {
552 1
            $params[SecurityToken::inst()->getName()] = SecurityToken::inst()->getValue();
553 1
        }
554 6
        return self::config()->url_segment . '/' .
555 6
        $action . '/' .
556 6
        $buyable->class . "/" .
557 6
        $buyable->ID .
558 6
        self::params_to_get_string($params);
559
    }
560
561
    /**
562
     * Creates the appropriate string parameters for links from array
563
     *
564
     * Produces string such as: MyParam%3D11%26OtherParam%3D1
565
     *     ...which decodes to: MyParam=11&OtherParam=1
566
     *
567
     * you will need to decode the url with javascript before using it.
568
     */
569 6
    protected static function params_to_get_string($array)
570
    {
571 6
        if ($array & count($array > 0)) {
572 3
            array_walk($array, create_function('&$v,$k', '$v = $k."=".$v ;'));
573 3
            return "?" . implode("&", $array);
574
        }
575 6
        return "";
576
    }
577
578
    /**
579
     * This is used here and in VariationForm and AddProductForm
580
     *
581
     * @param bool|string $status
582
     *
583
     * @return bool
584
     */
585 4
    public static function direct($status = true)
586
    {
587 4
        if (Director::is_ajax()) {
588
            return $status;
589
        }
590 4
        if (self::config()->direct_to_cart_page && $cartlink = CartPage::find_link()) {
591
            Controller::curr()->redirect($cartlink);
592
            return;
593
        } else {
594 4
            Controller::curr()->redirectBack();
595 4
            return;
596
        }
597
    }
598
599 4
    public function init()
600
    {
601 4
        parent::init();
602 4
        $this->cart = ShoppingCart::singleton();
603 4
    }
604
605
    /**
606
     * @return Product|ProductVariation|Buyable
607
     */
608 11
    protected function buyableFromRequest()
0 ignored issues
show
Complexity introduced by
This operation has 480 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
609
    {
610 4
        $request = $this->getRequest();
611
        if (
612 4
            SecurityToken::is_enabled() &&
613 4
            !self::config()->disable_security_token &&
614 1
            !SecurityToken::inst()->checkRequest($request)
615 4
        ) {
616 1
            return $this->httpError(
617 1
                400,
618 1
                _t("ShoppingCart.InvalidSecurityToken", "Invalid security token, possible CSRF attack.")
619 1
            );
620
        }
621 4
        $id = (int)$request->param('ID');
622 4
        if (empty($id)) {
623
            //TODO: store error message
624
            return null;
625
        }
626 4
        $buyableclass = "Product";
627 4
        if ($class = $request->param('Buyable')) {
628 11
            $buyableclass = Convert::raw2sql($class);
629 4
        }
630 4
        if (!ClassInfo::exists($buyableclass)) {
0 ignored issues
show
Bug introduced by
It seems like $buyableclass defined by \Convert::raw2sql($class) on line 628 can also be of type array; however, ClassInfo::exists() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
631
            //TODO: store error message
632
            return null;
633
        }
634
        //ensure only live products are returned, if they are versioned
635 4
        $buyable = Object::has_extension($buyableclass, 'Versioned')
0 ignored issues
show
Bug introduced by
It seems like $buyableclass defined by \Convert::raw2sql($class) on line 628 can also be of type array; however, Object::has_extension() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
636
            ?
637 4
            Versioned::get_by_stage($buyableclass, 'Live')->byID($id)
0 ignored issues
show
Bug introduced by
It seems like $buyableclass defined by \Convert::raw2sql($class) on line 628 can also be of type array; however, Versioned::get_by_stage() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
638 4
            :
639 4
            DataObject::get($buyableclass)->byID($id);
0 ignored issues
show
Bug introduced by
It seems like $buyableclass defined by \Convert::raw2sql($class) on line 628 can also be of type array; however, DataObject::get() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
640 4
        if (!$buyable || !($buyable instanceof Buyable)) {
641
            //TODO: store error message
642 1
            return null;
643
        }
644
645 4
        return $this->cart->getCorrectBuyable($buyable);
646
    }
647
648
    /**
649
     * Action: add item to cart
650
     *
651
     * @param SS_HTTPRequest $request
652
     *
653
     * @return SS_HTTPResponse
654
     */
655 4
    public function add($request)
656
    {
657 4
        if ($product = $this->buyableFromRequest()) {
658 4
            $quantity = (int)$request->getVar('quantity');
659 4
            if (!$quantity) {
660 4
                $quantity = 1;
661 4
            }
662 4
            $this->cart->add($product, $quantity, $request->getVars());
663 4
        }
664
665 4
        $this->updateLocale($request);
666 4
        $this->extend('updateAddResponse', $request, $response, $product, $quantity);
667 4
        return $response ? $response : self::direct();
668
    }
669
670
    /**
671
     * Action: remove a certain number of items from the cart
672
     *
673
     * @param SS_HTTPRequest $request
674
     *
675
     * @return SS_HTTPResponse
676
     */
677 1
    public function remove($request)
678
    {
679 1
        if ($product = $this->buyableFromRequest()) {
680 1
            $this->cart->remove($product, $quantity = 1, $request->getVars());
681 1
        }
682
683 1
        $this->updateLocale($request);
684 1
        $this->extend('updateRemoveResponse', $request, $response, $product, $quantity);
685 1
        return $response ? $response : self::direct();
686
    }
687
688
    /**
689
     * Action: remove all of an item from the cart
690
     *
691
     * @param SS_HTTPRequest $request
692
     *
693
     * @return SS_HTTPResponse
694
     */
695 1
    public function removeall($request)
696
    {
697 1
        if ($product = $this->buyableFromRequest()) {
698 1
            $this->cart->remove($product, null, $request->getVars());
699 1
        }
700
701 1
        $this->updateLocale($request);
702 1
        $this->extend('updateRemoveAllResponse', $request, $response, $product);
703 1
        return $response ? $response : self::direct();
704
    }
705
706
    /**
707
     * Action: update the quantity of an item in the cart
708
     *
709
     * @param SS_HTTPRequest $request
710
     *
711
     * @return AjaxHTTPResponse|bool
712
     */
713 2
    public function setquantity($request)
714
    {
715 2
        $product = $this->buyableFromRequest();
716 2
        $quantity = (int)$request->getVar('quantity');
717 2
        if ($product) {
718 2
            $this->cart->setQuantity($product, $quantity, $request->getVars());
719 2
        }
720
721 2
        $this->updateLocale($request);
722 2
        $this->extend('updateSetQuantityResponse', $request, $response, $product, $quantity);
723 2
        return $response ? $response : self::direct();
724
    }
725
726
    /**
727
     * Action: clear the cart
728
     *
729
     * @param SS_HTTPRequest $request
730
     *
731
     * @return AjaxHTTPResponse|bool
732
     */
733
    public function clear($request)
734
    {
735
        $this->updateLocale($request);
736
        $this->cart->clear();
737
        $this->extend('updateClearResponse', $request, $response);
738
        return $response ? $response : self::direct();
739
    }
740
741
    /**
742
     * Handle index requests
743
     */
744
    public function index()
745
    {
746
        if ($cart = $this->Cart()) {
0 ignored issues
show
Documentation Bug introduced by
The method Cart does not exist on object<ShoppingCart_Controller>? 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...
747
            $this->redirect($cart->CartLink);
748
            return;
749
        } elseif ($response = ErrorPage::response_for(404)) {
750
            return $response;
751
        }
752
        return $this->httpError(404, _t("ShoppingCart.NoCartInitialised", "no cart initialised"));
753
    }
754
755
    /**
756
     * Displays order info and cart contents.
757
     */
758
    public function debug()
759
    {
760
        if (Director::isDev() || Permission::check("ADMIN")) {
761
            //TODO: allow specifying a particular id to debug
762
            Requirements::css(SHOP_DIR . "/css/cartdebug.css");
763
            $order = ShoppingCart::curr();
764
            $content = ($order)
765
                ?
766
                Debug::text($order)
0 ignored issues
show
Documentation introduced by
$order is of type object<Order>, but the function expects a object<unknown_type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
767
                :
768
                "Cart has not been created yet. Add a product.";
769
            return array('Content' => $content);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('Content' => $content); (array<string,unknown|string>) is incompatible with the return type of the parent method ViewableData::Debug of type ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
770
        }
771
    }
772
773 4
    protected function updateLocale($request)
774
    {
775 4
        $order = $this->cart->current();
776 4
        if ($request && $request->isAjax() && $order) {
777
            ShopTools::install_locale($order->Locale);
778
        }
779 4
    }
780
}
781