* @license PHP License * @package WB * @subpackage base */ WBClass::load('WBShop'); /** * Shopping Cart * * Manage multiple shopping carts for each user. Logged in users may load, store etc. carts, * Anonymous user have onle ony cart in session. * * * @version 0.5.0 * @package WB * @subpackage base */ class WBShop_Cart extends WBStdClass { /** * * @var WBDatasource_Table */ private $table; /** * user's id to work on behalf of * @var string */ private $uid = '0'; /** * id of current cart * @var string */ private $id = '__new'; /** * current cart * @var array */ private $cart = array(); /** * list of items in cart * @var array */ private $item = null; /** * session storage * @var patSession_Storage */ private $sess; /** * @const session identifier for cart */ const SESS_CART = 'WBShop_Cart:cart'; /** * @const session identifier for cart items */ const SESS_ITEM = 'WBShop_Cart:item'; /** * cart filter * @var WBShop_Cart_Filter_Composite */ private $filter; /** * Cart config * @var WBConfig */ private $config; /** * constructor * * Instantiate table object */ public function __construct() { $this->table = WBClass::create('WBDatasource_Table'); $this->sess = WBClass::create('patSession'); $this->config = WBClass::create('WBConfig'); $this->config->load('shop/cart/config'); $this->startFilter(); // start with an empty cart $this->raw2cart(array()); } /** * Start cart filter(s) * * Create composite filter. Start all configured filter and set parameters. * Finally attach filter to composite. * * @throws WBException_Config */ private function startFilter() { // composite filter $this->filter = WBClass::create('WBShop_Cart_Filter_Composite'); // config of actual worker filters $filter = $this->config->get('cart/filter', array()); if (!is_array($filter)) { return; } // start filter foreach ($filter as $f) { if (!is_array($f)) { WBClass::load('WBException_Config'); throw new WBException_Config('Invalid filter config!', 1, __CLASS__); } // create filter $o = WBClass::create('WBShop_Cart_Filter_' . $f['name']); if (!$o) { WBClass::load('WBException_Config'); throw new WBException_Config('Could not start filter: ' . $f['name'], 2, __CLASS__); } // configure filter if (is_array($f['params'])) { $o->setParams($f['params']); } // attach filter $this->filter->add($o); } } /** * set current cart user * * @param string $uid * @return WBShop_Cart */ public function setUser($uid) { $this->uid = $uid; $this->cart[$this->table->getIdentifier(WBShop::TABLE_USER)] = $uid; return $this; } /** * Get list of user's shopping carts * * Fetch user's carts with status. Optionally exclude one cart * * @param array $status * @param string $exclude * @return array */ public function getList($status = array('saved', 'active'), $exclude = null) { $clause = array(); $clause[] = array( 'field' => 'status', 'relation' => 'in', 'value' => $status ); if ($exclude) { $clause[] = array( 'field' => $this->table->getIdentifier(WBShop::TABLE_CART), 'relation' => 'not', 'value' => $exclude ); } return $this->table->get(WBShop::TABLE_CART, null, $this->uid, $clause); } /** * Load active shopping cart * * Try to load active cart of current user. In case there is no current user, load empty * cart. * * @return WBShop_Cart */ public function loadActive() { $this->item = null; // load cart from session if ($this->sess->has(self::SESS_CART)) { $this->raw2cart(array($this->sess->get(self::SESS_CART))); if ($this->uid) { $this->moveFromSession2Db(); } return $this; } // there is no user if (!$this->uid) { $this->raw2cart(array()); return $this; } $clause = array(); $clause[] = array( 'field' => 'status', 'value' => 'active' ); $list = $this->table->get(WBShop::TABLE_CART, null, $this->uid, $clause); $this->raw2cart($list); $this->cart['status'] = 'active'; return $this; } /** * Load cart by id * * Try to load user's cart by cart id. * * @return WBShop_Cart * @param string $id * @param bool $public */ public function load($id, $public = false) { $this->item = null; $clause = array(); if ($public) { // load public cart $clause[] = array( 'field' => 'public', 'value' => 1 ); } else if (!$this->uid) { // there is no user $this->raw2cart(array()); return $this; } else { // add user id to clause $uPrimary = $this->table->getIdentifier(WBShop::TABLE_USER); $clause[] = array( 'field' => $uPrimary, 'value' => $this->uid ); } $list = $this->table->get(WBShop::TABLE_CART, $id, null, $clause); $this->raw2cart($list); return $this; } /** * Merge other cart with this one * * Current cart's status must be "saved" * Append title and note to origins' title and note. * Set cart as public of both carts were public before. * * @todo Reconsider whether to merge attributes instead of overriding them * @param WBShop_Cart * @return WBShop_Cart */ public function merge($cart) { $required = array( 'saved', 'active' ); if (!$this->updateStatus('', $required)) { throw new WBException_Call('Status "saved" / "active" required to merge into cart.'); } $this->setTitle($this->getTitle() . ' ' . $cart->getTitle()); $this->setNote($this->getNote() . ' ' . $cart->getNote()); $this->setPublic($this->getPublic() & $cart->getPublic()); // override attributes $this->setAttributes($cart->get()); $src = $cart->getItems(); $des = $this->getItems(); $aPrimary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); foreach ($src as $s) { $found = false; foreach ($des as $d) { if ($d[$aPrimary] == $s[$aPrimary]) { $found = true; $comment = $d['comment'] . $s['comment']; $this->set($s[$aPrimary], $d['quantity'] + $s['quantity'], $d['comment'] . $s['comment']); $this->setItemAttributes($s[$aPrimary], $s); break; } } if ($found) { continue; } $this->set($s[$aPrimary], $s['quantity'], $s['comment']); $this->setItemAttributes($s[$aPrimary], $s); } return $this; } /** * Sat carts title attribute * * @param string $title * @return WBShop_Cart */ public function setTitle($title = '') { if (!$this->id) { return $this; } $this->cart['title'] = trim($title); return $this; } /** * Get cart's title string * * @return string */ public function getTitle() { return $this->cart['title']; } /** * Sat carts note attribute * * @param string $note * @return WBShop_Cart */ public function setNote($note = '') { if (!$this->id) { return $this; } $this->cart['note'] = trim($note); return $this; } /** * Get cart's note string * * @return string */ public function getNote() { return $this->cart['note']; } /** * Mark cart as public * * @param bool $public * @return WBShop_Cart */ public function setPublic($public = true) { if (!$this->id) { return $this; } $this->cart['public'] = intval($public); return $this; } /** * Tell whether cart is public * * @return bool */ public function getPublic() { return $this->cart['public']; } /** * Set cart's custom attributes * * @param array $data * @return WBShop_Cart */ public function setAttributes($data) { $primary = $this->table->getIdentifier(WBShop::TABLE_CART); $uPrimary = $this->table->getIdentifier(WBShop::TABLE_USER); $invalid = array( $primary, 'id', 'title', 'note', 'public', 'created', 'changed', 'status', $uPrimary ); foreach ($data as $k => $v) { if (in_array($k, $invalid)) { unset($data[$k]); } } $this->cart = array_merge($this->cart, $data); return $this; } /** * Set item's custom attributes * * @param string $item article id * @param array $data * @return WBShop_Cart */ public function setItemAttributes($item, $data) { $this->loadItems(); if (empty($this->item)) { return $this; } $primary = $this->table->getIdentifier(WBShop::TABLE_CART); $aPrimary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); $invalid = array( $primary, $aPrimary, 'quantity', 'comment', 'created' ); foreach ($data as $k => $v) { if (in_array($k, $invalid)) { unset($data[$k]); } } foreach ($this->item as &$i) { if ($i[$aPrimary] == $item) { $i = array_merge($i, $data); $i['__action'] = '__update'; break; } } return $this; } /** * Delete cart * * Remove current cart. * * @return WBShop_Cart */ public function delete() { if (!$this->id) { return $this; } $reqiured = array( 'saved', 'active', ); if (!$this->updateStatus('', $reqiured)) { return $this; } $cart = $this->cart; $cart['id'] = $this->id; $this->table->delete(WBShop::TABLE_CART, $this->id); $this->item = null; $this->cart = array(); $this->id = null; WBEvent::trigger('shop:cart:delete', 'Deleted shopping cart', $cart); return $this; } /** * Cancel order * * * @return WBShop_Cart */ public function cancel() { $reqiured = array( 'ordered', 'postponed' ); $this->updateStatus('canceled', $reqiured); return $this; } /** * Order cart * * @todo allow to order cart from session for anonymous users * @return WBShop_Cart */ public function order() { $reqiured = array( 'saved', 'active', 'postponed' ); $this->updateStatus('ordered', $reqiured); return $this; } /** * Mark current cart as active * * @return WBShop_Cart */ public function activate() { $reqiured = array( 'saved' ); if (!$this->updateStatus('active', $reqiured)) { return $this; } $this->deactivateOthers($this->id); return $this; } /** * Deactivate all other carts * * Set all carts to "saved" and spare the one with given id * * @param string $id */ private function deactivateOthers($id) { $save = array( 'status' => 'saved' ); $clause = array(); $clause[] = array( 'field' => $this->table->getIdentifier(WBShop::TABLE_CART), 'relation' => 'not', 'value' => $id ); $clause[] = array( 'field' => $this->table->getIdentifier(WBShop::TABLE_USER), 'value' => $this->uid ); $clause[] = array( 'field' => 'status', 'value' => 'active' ); $this->table->save(WBShop::TABLE_CART, null, $save, $clause); } /** * Remove active flag from current cart * * @return WBShop_Cart */ public function deactivate() { $reqiured = array( 'active' ); $this->updateStatus('saved', $reqiured); return $this; } /** * add article to cart * * Either add item or change quantity and or comment * * @param string $item article id * @param int $quantity * @param string $comment * @return WBShop_Cart */ public function add($item, $quantity = 1, $comment = '') { $this->loadItems(); $primary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); foreach ($this->item as &$i) { if ($i[$primary] != $item) { continue; } $quantity += $i['quantity']; $comment = $i['comment'] . $comment; return $this->set($item, $quantity, $comment); } return $this->set($item, $quantity, $comment); } /** * set article in cart * * Either add item or update quantity or comment * * @param string $item article id * @param int $quantity * @param string $comment * @return WBShop_Cart */ public function set($item, $quantity = 1, $comment = '') { $this->loadItems(); $primary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); foreach ($this->item as &$i) { if ($i[$primary] != $item) { continue; } $i['quantity'] = $quantity; $i['comment'] = $comment; $i['__action'] = '__update'; if (1 > $i['quantity']) { $i['__action'] = '__delete'; } return $this; } $this->item[] = array( $primary => $item, 'quantity' => $quantity, 'comment' => $comment, '__action' => '__new' ); return $this; } /** * remove item from cart * * @var string $item article id * @return WBShop_Cart */ public function remove($item) { $this->loadItems(); $primary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); foreach ($this->item as &$i) { if ($i[$primary] != $item) { continue; } $i['__action'] = '__delete'; return $this; } } /** * save cart and all items * * Store current cart as well as all cart items in database. * @return WBShop_Cart */ public function save() { if (!$this->uid) { $this->saveInSession(); return $this; } $this->saveInDatabase(); // apply filter $this->filter->onCartLoad($this->cart); foreach ($this->item as &$i) { $this->filter->onItemLoad($i, $this->cart); } return $this; } /** * Store cart and items in session * * This is for anonymouse users */ private function saveInSession() { // save cart items $aPrimary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); $cPrimary = $this->table->getIdentifier(WBShop::TABLE_CART); $cart = $this->cart; $cart['id'] = 'session'; $cart[$cPrimary] = 'session'; $cart['status'] = 'active'; $this->filter->onCartSave($cart); $this->sess->set(self::SESS_CART, $cart); if (!$this->item) { $this->item = array(); } foreach ($this->item as $i => &$item) { if (!isset($item['__action'])) { continue; } switch ($item['__action']) { case '__update': case '__new': unset($item['__action']); $this->filter->onItemSave($item, $this->cart); break; case '__delete': unset($this->item[$i]); break; default: // this should never happen break; } } $this->item = array_values($this->item); $this->sess->set(self::SESS_ITEM, $this->item); } /** * Store cart and items in database * * Allow to move cart from session to database */ private function saveInDatabase() { $aPrimary = $this->table->getIdentifier(WBShop::TABLE_ARTICLE); $cPrimary = $this->table->getIdentifier(WBShop::TABLE_CART); $uPrimary = $this->table->getIdentifier(WBShop::TABLE_USER); // save cart $save = $this->cart; $this->filter->onCartSave($save); $saveable = array( 'title', 'note', 'public', 'status', 'created', 'changed', $uPrimary ); $saveable = array_merge($saveable, $this->filter->getSaveableCartAttributes()); foreach ($save as $k => $v) { if (in_array($k, $saveable)) { continue; } unset($save[$k]); } $this->id = $this->table->save(WBShop::TABLE_CART, $this->id, $save); // save cart items $insert = array(); $delete = array(); if (!$this->item) { return; } $saveable = array( 'created', 'quantity', 'comment', $cPrimary, $aPrimary ); $saveable = array_merge($saveable, $this->filter->getSaveableItemAttributes()); foreach ($this->item as $i => &$item) { if (!isset($item['__action'])) { continue; } switch ($item['__action']) { case '__new': $item[$cPrimary] = $this->id; $save = array(); $tmp = $item; $this->filter->onItemSave($tmp, $this->cart); foreach ($tmp as $k => $v) { if (in_array($k, $saveable)) { $save[$k] = $v; } } $insert[] = $save; break; case '__update': $clause = array(); $clause[] = array( 'field' => $aPrimary, 'value' => $item[$aPrimary] ); $clause[] = array( 'field' => $cPrimary, 'value' => $this->id ); $save = array(); $tmp = $item; $this->filter->onItemSave($tmp, $this->cart); foreach ($tmp as $k => $v) { if (in_array($k, $saveable)) { $save[$k] = $v; } } $this->table->save(WBShop::TABLE_CARTITEM, null, $save, $clause); break; case '__delete': unset($item['__action']); $delete[] = $item[$aPrimary]; unset($this->item[$i]); break; default: // this should never happen break; } } $this->item = array_values($this->item); if (!empty($delete)) { $clause = array(); $clause[] = array( 'field' => $aPrimary, 'relation' => 'in', 'value' => $delete ); $this->table->delete(WBShop::TABLE_CARTITEM, null, null, $clause); } if( !empty($insert)) { $this->table->save(WBShop::TABLE_CARTITEM, '__new', $insert); } } /** * Move active cart from session to database * * Whenever a user is logs in, move the session based cart to databse */ private function moveFromSession2Db() { // move from session to DB if ('session' != $this->id) { return; } // prepare cart items $this->loadItems(); foreach ($this->item as &$item) { if (!isset($item['__action'])) { $item['__action'] = '__new'; } } // prepare cart $this->id = '__new'; $uPrimary = $this->table->getIdentifier(WBShop::TABLE_USER); $this->cart[$uPrimary] = $this->uid; $this->cart['status'] = 'active'; // save active cart and deactivate others $this->saveInDatabase(); $this->deactivateOthers($this->id); // remove records from session $this->sess->clear(self::SESS_CART); $this->sess->clear(self::SESS_ITEM); } /** * Get id * * Get id of current cart */ public function getId() { return $this->id; } /** * Get cart data * * Get summarized information on current cart * * @return array */ public function get() { $this->loadItems(); $quantity = 0; foreach ($this->item as $i) { $quantity += $i['quantity']; } $cart = $this->cart; $cart['id'] = $this->id; $cart['items'] = count($this->item); $cart['items_quantity'] = $quantity; $cart['price_sum'] = 0; $cart['price_total'] = 0; return $cart; } /** * Fetch items in cart * * @return array $list of items */ public function getItems() { $list = array(); $this->loadItems(); foreach ($this->item as $i) { if (isset($i['__action']) && '__delete' == $i['__action']) { continue; } $list[] = $i; } return $list; } /** * Helper method to update status * * Check for old status and update record in database. In case * new status is empty, there will bo not update, just the check. * * @param string $new status * @param array $required old status * @return bool true on success */ private function updateStatus($new, $required) { if (!$this->id) { return false; } if (!in_array($this->cart['status'], $required)) { return false; } if (empty($new)) { return true; } $save = array( 'status' => $new ); $this->table->save(WBShop::TABLE_CART, $this->id, $save); $this->cart['status'] = $new; $cart = $this->cart; $cart['id'] = $this->id; WBEvent::trigger('shop:cart:status:changed', 'Changed status of shopping cart', $cart); return true; } /** * convert database result to cart * * @param array $list */ private function raw2cart($list) { $primary = $this->table->getIdentifier(WBShop::TABLE_CART); $uPrimary = $this->table->getIdentifier(WBShop::TABLE_USER); if (1 != count($list)) { $list = array( array( $primary => '__new', 'id' => '__new', 'title' => '', 'note' => '', 'public' => 0, 'created' => gmdate('Y-m-d H:i:s'), 'changed' => gmdate('Y-m-d H:i:s'), 'status' => 'saved', $uPrimary => $this->uid ) ); } $this->id = $list[0][$primary]; unset($list[0][$primary]); unset($list[0]['id']); $this->cart = $list[0]; $this->filter->onCartLoad($this->cart); } /** * load cart items * * Load list of items either from database or session */ private function loadItems() { if ($this->item) { return; } if ('session' == $this->id) { $this->item = $this->sess->get(self::SESS_ITEM); } else { $this->item = $this->table->get(WBShop::TABLE_CARTITEM, null, $this->id); } foreach ($this->item as &$i) { $this->filter->onItemLoad($i, $this->cart); } } } ?>