Source of file ShoppingCart.php
Size: 15,134 Bytes - Last Modified: 2021-12-23T10:24:58+00:00
/var/www/docs.ssmods.com/process/src/src/Cart/ShoppingCart.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 | <?php namespace SilverShop\Cart; use Exception; use SilverShop\Extension\OrderManipulationExtension; use SilverShop\Extension\ProductVariationsExtension; use SilverShop\Model\Buyable; use SilverShop\Model\Order; use SilverShop\Model\OrderItem; use SilverShop\ORM\Filters\MatchObjectFilter; use SilverShop\Page\Product; use SilverShop\ShopTools; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Injector\Injectable; use SilverStripe\Security\Member; use SilverStripe\Security\Security; /** * Encapsulated manipulation of the current order using a singleton pattern. * * Ensures that an order is only started (persisted to DB) when necessary, * and all future changes are on the same order, until the order has is placed. * The requirement for starting an order is to adding an item to the cart. * * @package shop */ class ShoppingCart { use Injectable; use Configurable; private static $cartid_session_name = 'SilverShop.shoppingcartid'; /** * @var Order */ private $order; private $calculateonce = false; private $message; private $type; /** * Shortened alias for ShoppingCart::singleton()->current() * * @return Order */ public static function curr() { return self::singleton()->current(); } /** * Get the current order, or return null if it doesn't exist. * * @return Order */ public function current() { $session = ShopTools::getSession(); //find order by id saved to session (allows logging out and retaining cart contents) if (!$this->order && $sessionid = $session->get(self::config()->cartid_session_name)) { $this->order = Order::get()->filter( [ 'Status' => 'Cart', 'ID' => $sessionid, ] )->first(); } if (!$this->calculateonce && $this->order) { $this->order->calculate(); $this->calculateonce = true; } return $this->order ? $this->order : null; } /** * Set the current cart * * @param Order $cart the Order to use as the current cart-content * * @return ShoppingCart */ public function setCurrent(Order $cart) { if (!$cart->IsCart()) { trigger_error('Passed Order object is not cart status', E_ERROR); } $this->order = $cart; $session = ShopTools::getSession(); $session->set(self::config()->cartid_session_name, $cart->ID); return $this; } /** * Helper that only allows orders to be started internally. * * @return Order */ protected function findOrMake() { if ($this->current()) { return $this->current(); } $this->order = Order::create(); if (Member::config()->login_joins_cart && ($member = Security::getCurrentUser())) { $this->order->MemberID = $member->ID; } $this->order->write(); $this->order->extend('onStartOrder'); $session = ShopTools::getSession(); $session->set(self::config()->cartid_session_name, $this->order->ID); return $this->order; } /** * Adds an item to the cart * * @param Buyable $buyable * @param int $quantity * @param array $filter * * @return boolean|OrderItem false or the new/existing item */ public function add(Buyable $buyable, $quantity = 1, $filter = []) { $order = $this->findOrMake(); // If an extension throws an exception, error out try { $order->extend('beforeAdd', $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } if (!$buyable) { return $this->error(_t(__CLASS__ . '.ProductNotFound', 'Product not found.')); } $item = $this->findOrMakeItem($buyable, $quantity, $filter); if (!$item) { return false; } if (!$item->_brandnew) { $item->Quantity += $quantity; } else { $item->Quantity = $quantity; } // If an extension throws an exception, error out try { $order->extend('afterAdd', $item, $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } $item->write(); $this->message(_t(__CLASS__ . '.ItemAdded', 'Item has been added successfully.')); return $item; } /** * Remove an item from the cart. * * @param Buyable $buyable * @param int $quantity - number of items to remove, or leave null for all items (default) * @param array $filter * * @return boolean success/failure */ public function remove(Buyable $buyable, $quantity = null, $filter = []) { $order = $this->current(); if (!$order) { return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); } // If an extension throws an exception, error out try { $order->extend('beforeRemove', $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } $item = $this->get($buyable, $filter); if (!$item || !$this->removeOrderItem($item, $quantity)) { return false; } // If an extension throws an exception, error out // TODO: There should be a rollback try { $order->extend('afterRemove', $item, $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } $this->message(_t(__CLASS__ . '.ItemRemoved', 'Item has been successfully removed.')); return true; } /** * Remove a specific order item from cart * * @param OrderItem $item * @param int $quantity - number of items to remove or leave `null` to remove all items (default) * @return boolean success/failure */ public function removeOrderItem(OrderItem $item, $quantity = null) { $order = $this->current(); if (!$order) { return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); } if (!$item || $item->OrderID != $order->ID) { return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); } //if $quantity will become 0, then remove all if (!$quantity || ($item->Quantity - $quantity) <= 0) { $item->delete(); $item->destroy(); } else { $item->Quantity -= $quantity; $item->write(); } return true; } /** * Sets the quantity of an item in the cart. * Will automatically add or remove item, if necessary. * * @param Buyable $buyable * @param int $quantity * @param array $filter * * @return boolean|OrderItem false or the new/existing item */ public function setQuantity(Buyable $buyable, $quantity = 1, $filter = []) { if ($quantity <= 0) { return $this->remove($buyable, $quantity, $filter); } $item = $this->findOrMakeItem($buyable, $quantity, $filter); if (!$item || !$this->updateOrderItemQuantity($item, $quantity, $filter)) { return false; } return $item; } /** * Update quantity of a given order item * * @param OrderItem $item * @param int $quantity the new quantity to use * @param array $filter * @return boolean success/failure */ public function updateOrderItemQuantity(OrderItem $item, $quantity = 1, $filter = []) { $order = $this->current(); if (!$order) { return $this->error(_t(__CLASS__ . '.NoOrder', 'No current order.')); } if (!$item || $item->OrderID != $order->ID) { return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); } $buyable = $item->Buyable(); // If an extension throws an exception, error out try { $order->extend('beforeSetQuantity', $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } $item->Quantity = $quantity; // If an extension throws an exception, error out try { $order->extend('afterSetQuantity', $item, $buyable, $quantity, $filter); } catch (Exception $exception) { return $this->error($exception->getMessage()); } $item->write(); $this->message(_t(__CLASS__ . '.QuantitySet', 'Quantity has been set.')); return true; } /** * Finds or makes an order item for a given product + filter. * * @param Buyable $buyable the buyable * @param int $quantity quantity to add * @param array $filter * * @return OrderItem the found or created item * @throws \SilverStripe\ORM\ValidationException */ private function findOrMakeItem(Buyable $buyable, $quantity = 1, $filter = []) { $order = $this->findOrMake(); if (!$buyable || !$order) { return null; } $item = $this->get($buyable, $filter); if (!$item) { $member = Security::getCurrentUser(); $buyable = $this->getCorrectBuyable($buyable); if (!$buyable->canPurchase($member, $quantity)) { return $this->error( _t( __CLASS__ . '.CannotPurchase', 'This {Title} cannot be purchased.', '', ['Title' => $buyable->i18n_singular_name()] ) ); //TODO: produce a more specific message } $item = $buyable->createItem($quantity, $filter); $item->OrderID = $order->ID; $item->write(); $order->Items()->add($item); $item->_brandnew = true; // flag as being new } return $item; } /** * Finds an existing order item. * * @param Buyable $buyable * @param array $customfilter * * @return OrderItem the item requested or null */ public function get(Buyable $buyable, $customfilter = array()) { $order = $this->current(); if (!$buyable || !$order) { return null; } $buyable = $this->getCorrectBuyable($buyable); $filter = array( 'OrderID' => $order->ID, ); $itemclass = Config::inst()->get(get_class($buyable), 'order_item'); $relationship = Config::inst()->get($itemclass, 'buyable_relationship'); $filter[$relationship . 'ID'] = $buyable->ID; $required = ['OrderID', $relationship . 'ID']; if (is_array($itemclass::config()->required_fields)) { $required = array_merge($required, $itemclass::config()->required_fields); } $query = new MatchObjectFilter($itemclass, array_merge($customfilter, $filter), $required); $item = $itemclass::get()->where($query->getFilter())->first(); if (!$item) { return $this->error(_t(__CLASS__ . '.ItemNotFound', 'Item not found.')); } return $item; } /** * Ensure the proper buyable will be returned for a given buyable… * This is being used to ensure a product with variations cannot be added to the cart… * a Variation has to be added instead! * * @param Buyable $buyable * @return Buyable */ public function getCorrectBuyable(Buyable $buyable) { if ($buyable instanceof Product && $buyable->hasExtension(ProductVariationsExtension::class) && $buyable->Variations()->count() > 0 ) { foreach ($buyable->Variations() as $variation) { if ($variation->canPurchase()) { return $variation; } } } return $buyable; } /** * Store old cart id in session order history * * @param int|null $requestedOrderId optional parameter that denotes the order that was requested */ public function archiveorderid($requestedOrderId = null) { $session = ShopTools::getSession(); $sessionId = $session->get(self::config()->cartid_session_name); $order = Order::get() ->filter('Status:not', 'Cart') ->byId($sessionId); if ($order && !$order->IsCart()) { OrderManipulationExtension::add_session_order($order); } // in case there was no order requested // OR there was an order requested AND it's the same one as currently in the session, // then clear the cart. This check is here to prevent clearing of the cart if the user just // wants to view an old order (via AccountPage). if (!$requestedOrderId || ($sessionId == $requestedOrderId)) { $this->clear(); } } /** * Empty / abandon the entire cart. * * @param bool $write whether or not to write the abandoned order * @return bool - true if successful, false if no cart found */ public function clear($write = true) { $session = ShopTools::getSession(); $session->set(self::config()->cartid_session_name, null)->clear(self::config()->cartid_session_name); $order = $this->current(); $this->order = null; if ($write) { if (!$order) { return $this->error(_t(__CLASS__ . '.NoCartFound', 'No cart found.')); } $order->write(); } $this->message(_t(__CLASS__ . '.Cleared', 'Cart was successfully cleared.')); return true; } /** * Store a new error. */ protected function error($message) { $this->message($message, 'bad'); return null; } /** * Store a message to be fed back to user. * * @param string $message * @param string $type - good, bad, warning */ protected function message($message, $type = 'good') { $this->message = $message; $this->type = $type; } public function getMessage() { return $this->message; } public function getMessageType() { return $this->type; } public function clearMessage() { $this->message = null; } //singleton protection public function __clone() { trigger_error('Clone is not allowed.', E_USER_ERROR); } public function __wakeup() { trigger_error('Unserializing is not allowed.', E_USER_ERROR); } } |