Total Complexity | 72 |
Total Lines | 490 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like ShoppingCart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ShoppingCart, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
29 | class ShoppingCart |
||
30 | { |
||
31 | use Injectable; |
||
32 | use Configurable; |
||
33 | |||
34 | private static $cartid_session_name = 'SilverShop.shoppingcartid'; |
||
35 | |||
36 | /** |
||
37 | * @var Order |
||
38 | */ |
||
39 | private $order; |
||
40 | |||
41 | private $calculateonce = false; |
||
42 | |||
43 | private $message; |
||
44 | |||
45 | private $type; |
||
46 | |||
47 | |||
48 | /** |
||
49 | * Shortened alias for ShoppingCart::singleton()->current() |
||
50 | * |
||
51 | * @return Order |
||
52 | */ |
||
53 | public static function curr() |
||
54 | { |
||
55 | return self::singleton()->current(); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * Get the current order, or return null if it doesn't exist. |
||
60 | * |
||
61 | * @return Order |
||
62 | */ |
||
63 | public function current() |
||
64 | { |
||
65 | $session = ShopTools::getSession(); |
||
66 | //find order by id saved to session (allows logging out and retaining cart contents) |
||
67 | if (!$this->order && $sessionid = $session->get(self::config()->cartid_session_name)) { |
||
68 | $this->order = Order::get()->filter( |
||
69 | [ |
||
70 | 'Status' => 'Cart', |
||
71 | 'ID' => $sessionid, |
||
72 | ] |
||
73 | )->first(); |
||
74 | } |
||
75 | if (!$this->calculateonce && $this->order) { |
||
76 | $this->order->calculate(); |
||
77 | $this->calculateonce = true; |
||
78 | } |
||
79 | |||
80 | return $this->order ? $this->order : null; |
||
81 | } |
||
82 | |||
83 | /** |
||
84 | * Set the current cart |
||
85 | * |
||
86 | * @param Order $cart the Order to use as the current cart-content |
||
87 | * |
||
88 | * @return ShoppingCart |
||
89 | */ |
||
90 | public function setCurrent(Order $cart) |
||
91 | { |
||
92 | if (!$cart->IsCart()) { |
||
93 | trigger_error('Passed Order object is not cart status', E_ERROR); |
||
94 | } |
||
95 | $this->order = $cart; |
||
96 | $session = ShopTools::getSession(); |
||
97 | $session->set(self::config()->cartid_session_name, $cart->ID); |
||
98 | |||
99 | return $this; |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * Helper that only allows orders to be started internally. |
||
104 | * |
||
105 | * @return Order |
||
106 | */ |
||
107 | protected function findOrMake() |
||
108 | { |
||
109 | if ($this->current()) { |
||
110 | return $this->current(); |
||
111 | } |
||
112 | $this->order = Order::create(); |
||
113 | if (Member::config()->login_joins_cart && ($member = Security::getCurrentUser())) { |
||
114 | $this->order->MemberID = $member->ID; |
||
115 | } |
||
116 | $this->order->write(); |
||
117 | $this->order->extend('onStartOrder'); |
||
118 | |||
119 | $session = ShopTools::getSession(); |
||
120 | $session->set(self::config()->cartid_session_name, $this->order->ID); |
||
121 | |||
122 | return $this->order; |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Adds an item to the cart |
||
127 | * |
||
128 | * @param Buyable $buyable |
||
129 | * @param int $quantity |
||
130 | * @param array $filter |
||
131 | * |
||
132 | * @return boolean|OrderItem false or the new/existing item |
||
133 | */ |
||
134 | public function add(Buyable $buyable, $quantity = 1, $filter = []) |
||
170 | } |
||
171 | |||
172 | /** |
||
173 | * Remove an item from the cart. |
||
174 | * |
||
175 | * @param Buyable $buyable |
||
176 | * @param int $quantity - number of items to remove, or leave null for all items (default) |
||
177 | * @param array $filter |
||
178 | * |
||
179 | * @return boolean success/failure |
||
180 | */ |
||
181 | public function remove(Buyable $buyable, $quantity = null, $filter = []) |
||
182 | { |
||
183 | $order = $this->current(); |
||
184 | |||
185 | if (!$order) { |
||
186 | return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); |
||
187 | } |
||
188 | |||
189 | // If an extension throws an exception, error out |
||
190 | try { |
||
191 | $order->extend('beforeRemove', $buyable, $quantity, $filter); |
||
192 | } catch (Exception $exception) { |
||
193 | return $this->error($exception->getMessage()); |
||
194 | } |
||
195 | |||
196 | $item = $this->get($buyable, $filter); |
||
197 | |||
198 | if (!$item || !$this->removeOrderItem($item, $quantity)) { |
||
199 | return false; |
||
200 | } |
||
201 | |||
202 | // If an extension throws an exception, error out |
||
203 | // TODO: There should be a rollback |
||
204 | try { |
||
205 | $order->extend('afterRemove', $item, $buyable, $quantity, $filter); |
||
206 | } catch (Exception $exception) { |
||
207 | return $this->error($exception->getMessage()); |
||
208 | } |
||
209 | |||
210 | $this->message(_t(__CLASS__ . '.ItemRemoved', 'Item has been successfully removed.')); |
||
211 | |||
212 | return true; |
||
213 | } |
||
214 | |||
215 | /** |
||
216 | * Remove a specific order item from cart |
||
217 | * |
||
218 | * @param OrderItem $item |
||
219 | * @param int $quantity - number of items to remove or leave `null` to remove all items (default) |
||
220 | * @return boolean success/failure |
||
221 | */ |
||
222 | public function removeOrderItem(OrderItem $item, $quantity = null) |
||
223 | { |
||
224 | $order = $this->current(); |
||
225 | |||
226 | if (!$order) { |
||
227 | return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); |
||
228 | } |
||
229 | |||
230 | if (!$item || $item->OrderID != $order->ID) { |
||
231 | return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); |
||
232 | } |
||
233 | |||
234 | //if $quantity will become 0, then remove all |
||
235 | if (!$quantity || ($item->Quantity - $quantity) <= 0) { |
||
236 | $item->delete(); |
||
237 | $item->destroy(); |
||
238 | } else { |
||
239 | $item->Quantity -= $quantity; |
||
240 | $item->write(); |
||
241 | } |
||
242 | |||
243 | return true; |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * Sets the quantity of an item in the cart. |
||
248 | * Will automatically add or remove item, if necessary. |
||
249 | * |
||
250 | * @param Buyable $buyable |
||
251 | * @param int $quantity |
||
252 | * @param array $filter |
||
253 | * |
||
254 | * @return boolean|OrderItem false or the new/existing item |
||
255 | */ |
||
256 | public function setQuantity(Buyable $buyable, $quantity = 1, $filter = []) |
||
257 | { |
||
258 | if ($quantity <= 0) { |
||
259 | return $this->remove($buyable, $quantity, $filter); |
||
260 | } |
||
261 | |||
262 | $item = $this->findOrMakeItem($buyable, $quantity, $filter); |
||
263 | |||
264 | if (!$item || !$this->updateOrderItemQuantity($item, $quantity, $filter)) { |
||
265 | return false; |
||
266 | } |
||
267 | |||
268 | return $item; |
||
269 | } |
||
270 | |||
271 | /** |
||
272 | * Update quantity of a given order item |
||
273 | * |
||
274 | * @param OrderItem $item |
||
275 | * @param int $quantity the new quantity to use |
||
276 | * @param array $filter |
||
277 | * @return boolean success/failure |
||
278 | */ |
||
279 | public function updateOrderItemQuantity(OrderItem $item, $quantity = 1, $filter = []) |
||
280 | { |
||
281 | $order = $this->current(); |
||
282 | |||
283 | if (!$order) { |
||
284 | return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); |
||
285 | } |
||
286 | |||
287 | if (!$item || $item->OrderID != $order->ID) { |
||
288 | return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); |
||
289 | } |
||
290 | |||
291 | $buyable = $item->Buyable(); |
||
292 | // If an extension throws an exception, error out |
||
293 | try { |
||
294 | $order->extend('beforeSetQuantity', $buyable, $quantity, $filter); |
||
295 | } catch (Exception $exception) { |
||
296 | return $this->error($exception->getMessage()); |
||
297 | } |
||
298 | |||
299 | $item->Quantity = $quantity; |
||
300 | |||
301 | // If an extension throws an exception, error out |
||
302 | try { |
||
303 | $order->extend('afterSetQuantity', $item, $buyable, $quantity, $filter); |
||
304 | } catch (Exception $exception) { |
||
305 | return $this->error($exception->getMessage()); |
||
306 | } |
||
307 | |||
308 | $item->write(); |
||
309 | $this->message(_t(__CLASS__ . '.QuantitySet', 'Quantity has been set.')); |
||
310 | |||
311 | return true; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Finds or makes an order item for a given product + filter. |
||
316 | * |
||
317 | * @param Buyable $buyable the buyable |
||
318 | * @param int $quantity quantity to add |
||
319 | * @param array $filter |
||
320 | * |
||
321 | * @return OrderItem the found or created item |
||
322 | * @throws \SilverStripe\ORM\ValidationException |
||
323 | */ |
||
324 | private function findOrMakeItem(Buyable $buyable, $quantity = 1, $filter = []) |
||
325 | { |
||
326 | $order = $this->findOrMake(); |
||
327 | |||
328 | if (!$buyable || !$order) { |
||
329 | return null; |
||
330 | } |
||
331 | |||
332 | $item = $this->get($buyable, $filter); |
||
333 | |||
334 | if (!$item) { |
||
335 | $member = Security::getCurrentUser(); |
||
336 | |||
337 | $buyable = $this->getCorrectBuyable($buyable); |
||
338 | |||
339 | if (!$buyable->canPurchase($member, $quantity)) { |
||
340 | return $this->error( |
||
341 | _t( |
||
342 | __CLASS__ . '.CannotPurchase', |
||
343 | 'This {Title} cannot be purchased.', |
||
344 | '', |
||
345 | ['Title' => $buyable->i18n_singular_name()] |
||
346 | ) |
||
347 | ); |
||
348 | //TODO: produce a more specific message |
||
349 | } |
||
350 | |||
351 | $item = $buyable->createItem($quantity, $filter); |
||
352 | $item->OrderID = $order->ID; |
||
353 | $item->write(); |
||
354 | |||
355 | $order->Items()->add($item); |
||
356 | |||
357 | $item->_brandnew = true; // flag as being new |
||
358 | } |
||
359 | |||
360 | return $item; |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Finds an existing order item. |
||
365 | * |
||
366 | * @param Buyable $buyable |
||
367 | * @param array $customfilter |
||
368 | * |
||
369 | * @return OrderItem the item requested or null |
||
370 | */ |
||
371 | public function get(Buyable $buyable, $customfilter = array()) |
||
372 | { |
||
373 | $order = $this->current(); |
||
374 | if (!$buyable || !$order) { |
||
375 | return null; |
||
376 | } |
||
377 | |||
378 | $buyable = $this->getCorrectBuyable($buyable); |
||
379 | |||
380 | $filter = array( |
||
381 | 'OrderID' => $order->ID, |
||
382 | ); |
||
383 | |||
384 | $itemclass = Config::inst()->get(get_class($buyable), 'order_item'); |
||
385 | $relationship = Config::inst()->get($itemclass, 'buyable_relationship'); |
||
386 | $filter[$relationship . 'ID'] = $buyable->ID; |
||
387 | $required = ['OrderID', $relationship . 'ID']; |
||
388 | if (is_array($itemclass::config()->required_fields)) { |
||
389 | $required = array_merge($required, $itemclass::config()->required_fields); |
||
390 | } |
||
391 | $query = new MatchObjectFilter($itemclass, array_merge($customfilter, $filter), $required); |
||
392 | $item = $itemclass::get()->where($query->getFilter())->first(); |
||
393 | if (!$item) { |
||
394 | return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); |
||
395 | } |
||
396 | |||
397 | return $item; |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | * Ensure the proper buyable will be returned for a given buyable… |
||
402 | * This is being used to ensure a product with variations cannot be added to the cart… |
||
403 | * a Variation has to be added instead! |
||
404 | * |
||
405 | * @param Buyable $buyable |
||
406 | * @return Buyable |
||
407 | */ |
||
408 | public function getCorrectBuyable(Buyable $buyable) |
||
409 | { |
||
410 | if ($buyable instanceof Product |
||
411 | && $buyable->hasExtension(ProductVariationsExtension::class) |
||
412 | && $buyable->Variations()->count() > 0 |
||
413 | ) { |
||
414 | foreach ($buyable->Variations() as $variation) { |
||
415 | if ($variation->canPurchase()) { |
||
416 | return $variation; |
||
417 | } |
||
418 | } |
||
419 | } |
||
420 | |||
421 | return $buyable; |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * Store old cart id in session order history |
||
426 | * |
||
427 | * @param int|null $requestedOrderId optional parameter that denotes the order that was requested |
||
428 | */ |
||
429 | public function archiveorderid($requestedOrderId = null) |
||
430 | { |
||
431 | $session = ShopTools::getSession(); |
||
432 | $sessionId = $session->get(self::config()->cartid_session_name); |
||
433 | $order = Order::get() |
||
434 | ->filter('Status:not', 'Cart') |
||
435 | ->byId($sessionId); |
||
436 | |||
437 | if ($order && !$order->IsCart()) { |
||
438 | OrderManipulationExtension::add_session_order($order); |
||
439 | } |
||
440 | // in case there was no order requested |
||
441 | // OR there was an order requested AND it's the same one as currently in the session, |
||
442 | // then clear the cart. This check is here to prevent clearing of the cart if the user just |
||
443 | // wants to view an old order (via AccountPage). |
||
444 | if (!$requestedOrderId || ($sessionId == $requestedOrderId)) { |
||
445 | $this->clear(); |
||
446 | } |
||
447 | } |
||
448 | |||
449 | /** |
||
450 | * Empty / abandon the entire cart. |
||
451 | * |
||
452 | * @param bool $write whether or not to write the abandoned order |
||
453 | * @return bool - true if successful, false if no cart found |
||
454 | */ |
||
455 | public function clear($write = true) |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * Store a new error. |
||
475 | */ |
||
476 | protected function error($message) |
||
477 | { |
||
478 | $this->message($message, 'bad'); |
||
479 | |||
480 | return null; |
||
481 | } |
||
482 | |||
483 | /** |
||
484 | * Store a message to be fed back to user. |
||
485 | * |
||
486 | * @param string $message |
||
487 | * @param string $type - good, bad, warning |
||
488 | */ |
||
489 | protected function message($message, $type = 'good') |
||
490 | { |
||
491 | $this->message = $message; |
||
492 | $this->type = $type; |
||
493 | } |
||
494 | |||
495 | public function getMessage() |
||
496 | { |
||
497 | return $this->message; |
||
498 | } |
||
499 | |||
500 | public function getMessageType() |
||
503 | } |
||
504 | |||
505 | public function clearMessage() |
||
506 | { |
||
507 | $this->message = null; |
||
508 | } |
||
509 | |||
510 | //singleton protection |
||
511 | public function __clone() |
||
514 | } |
||
515 | |||
516 | public function __wakeup() |
||
517 | { |
||
521 |
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.