Cart.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. <?php
  2. /**
  3. * CodeIgniter
  4. *
  5. * An open source application development framework for PHP
  6. *
  7. * This content is released under the MIT License (MIT)
  8. *
  9. * Copyright (c) 2014 - 2019, British Columbia Institute of Technology
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in
  19. * all copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. * THE SOFTWARE.
  28. *
  29. * @package CodeIgniter
  30. * @author EllisLab Dev Team
  31. * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
  32. * @copyright Copyright (c) 2014 - 2019, British Columbia Institute of Technology (http://bcit.ca/)
  33. * @license http://opensource.org/licenses/MIT MIT License
  34. * @link https://codeigniter.com
  35. * @since Version 1.0.0
  36. * @filesource
  37. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * Shopping Cart Class
  41. *
  42. * @package CodeIgniter
  43. * @subpackage Libraries
  44. * @category Shopping Cart
  45. * @author EllisLab Dev Team
  46. * @link https://codeigniter.com/user_guide/libraries/cart.html
  47. * @deprecated 3.0.0 This class is too specific for CI.
  48. */
  49. class CI_Cart {
  50. /**
  51. * These are the regular expression rules that we use to validate the product ID and product name
  52. * alpha-numeric, dashes, underscores, or periods
  53. *
  54. * @var string
  55. */
  56. public $product_id_rules = '\.a-z0-9_-';
  57. /**
  58. * These are the regular expression rules that we use to validate the product ID and product name
  59. * alpha-numeric, dashes, underscores, colons or periods
  60. *
  61. * @var string
  62. */
  63. public $product_name_rules = '\w \-\.\:';
  64. /**
  65. * only allow safe product names
  66. *
  67. * @var bool
  68. */
  69. public $product_name_safe = TRUE;
  70. // --------------------------------------------------------------------------
  71. /**
  72. * Reference to CodeIgniter instance
  73. *
  74. * @var object
  75. */
  76. protected $CI;
  77. /**
  78. * Contents of the cart
  79. *
  80. * @var array
  81. */
  82. protected $_cart_contents = array();
  83. /**
  84. * Shopping Class Constructor
  85. *
  86. * The constructor loads the Session class, used to store the shopping cart contents.
  87. *
  88. * @param array
  89. * @return void
  90. */
  91. public function __construct($params = array())
  92. {
  93. // Set the super object to a local variable for use later
  94. $this->CI =& get_instance();
  95. // Are any config settings being passed manually? If so, set them
  96. $config = is_array($params) ? $params : array();
  97. // Load the Sessions class
  98. $this->CI->load->driver('session', $config);
  99. // Grab the shopping cart array from the session table
  100. $this->_cart_contents = $this->CI->session->userdata('cart_contents');
  101. if ($this->_cart_contents === NULL)
  102. {
  103. // No cart exists so we'll set some base values
  104. $this->_cart_contents = array('cart_total' => 0, 'total_items' => 0);
  105. }
  106. log_message('info', 'Cart Class Initialized');
  107. }
  108. // --------------------------------------------------------------------
  109. /**
  110. * Insert items into the cart and save it to the session table
  111. *
  112. * @param array
  113. * @return bool
  114. */
  115. public function insert($items = array())
  116. {
  117. // Was any cart data passed? No? Bah...
  118. if ( ! is_array($items) OR count($items) === 0)
  119. {
  120. log_message('error', 'The insert method must be passed an array containing data.');
  121. return FALSE;
  122. }
  123. // You can either insert a single product using a one-dimensional array,
  124. // or multiple products using a multi-dimensional one. The way we
  125. // determine the array type is by looking for a required array key named "id"
  126. // at the top level. If it's not found, we will assume it's a multi-dimensional array.
  127. $save_cart = FALSE;
  128. if (isset($items['id']))
  129. {
  130. if (($rowid = $this->_insert($items)))
  131. {
  132. $save_cart = TRUE;
  133. }
  134. }
  135. else
  136. {
  137. foreach ($items as $val)
  138. {
  139. if (is_array($val) && isset($val['id']))
  140. {
  141. if ($this->_insert($val))
  142. {
  143. $save_cart = TRUE;
  144. }
  145. }
  146. }
  147. }
  148. // Save the cart data if the insert was successful
  149. if ($save_cart === TRUE)
  150. {
  151. $this->_save_cart();
  152. return isset($rowid) ? $rowid : TRUE;
  153. }
  154. return FALSE;
  155. }
  156. // --------------------------------------------------------------------
  157. /**
  158. * Insert
  159. *
  160. * @param array
  161. * @return bool
  162. */
  163. protected function _insert($items = array())
  164. {
  165. // Was any cart data passed? No? Bah...
  166. if ( ! is_array($items) OR count($items) === 0)
  167. {
  168. log_message('error', 'The insert method must be passed an array containing data.');
  169. return FALSE;
  170. }
  171. // --------------------------------------------------------------------
  172. // Does the $items array contain an id, quantity, price, and name? These are required
  173. if ( ! isset($items['id'], $items['qty'], $items['price'], $items['name']))
  174. {
  175. log_message('error', 'The cart array must contain a product ID, quantity, price, and name.');
  176. return FALSE;
  177. }
  178. // --------------------------------------------------------------------
  179. // Prep the quantity. It can only be a number. Duh... also trim any leading zeros
  180. $items['qty'] = (float) $items['qty'];
  181. // If the quantity is zero or blank there's nothing for us to do
  182. if ($items['qty'] == 0)
  183. {
  184. return FALSE;
  185. }
  186. // --------------------------------------------------------------------
  187. // Validate the product ID. It can only be alpha-numeric, dashes, underscores or periods
  188. // Not totally sure we should impose this rule, but it seems prudent to standardize IDs.
  189. // Note: These can be user-specified by setting the $this->product_id_rules variable.
  190. if ( ! preg_match('/^['.$this->product_id_rules.']+$/i', $items['id']))
  191. {
  192. log_message('error', 'Invalid product ID. The product ID can only contain alpha-numeric characters, dashes, and underscores');
  193. return FALSE;
  194. }
  195. // --------------------------------------------------------------------
  196. // Validate the product name. It can only be alpha-numeric, dashes, underscores, colons or periods.
  197. // Note: These can be user-specified by setting the $this->product_name_rules variable.
  198. if ($this->product_name_safe && ! preg_match('/^['.$this->product_name_rules.']+$/i'.(UTF8_ENABLED ? 'u' : ''), $items['name']))
  199. {
  200. log_message('error', 'An invalid name was submitted as the product name: '.$items['name'].' The name can only contain alpha-numeric characters, dashes, underscores, colons, and spaces');
  201. return FALSE;
  202. }
  203. // --------------------------------------------------------------------
  204. // Prep the price. Remove leading zeros and anything that isn't a number or decimal point.
  205. $items['price'] = (float) $items['price'];
  206. // We now need to create a unique identifier for the item being inserted into the cart.
  207. // Every time something is added to the cart it is stored in the master cart array.
  208. // Each row in the cart array, however, must have a unique index that identifies not only
  209. // a particular product, but makes it possible to store identical products with different options.
  210. // For example, what if someone buys two identical t-shirts (same product ID), but in
  211. // different sizes? The product ID (and other attributes, like the name) will be identical for
  212. // both sizes because it's the same shirt. The only difference will be the size.
  213. // Internally, we need to treat identical submissions, but with different options, as a unique product.
  214. // Our solution is to convert the options array to a string and MD5 it along with the product ID.
  215. // This becomes the unique "row ID"
  216. if (isset($items['options']) && count($items['options']) > 0)
  217. {
  218. $rowid = md5($items['id'].serialize($items['options']));
  219. }
  220. else
  221. {
  222. // No options were submitted so we simply MD5 the product ID.
  223. // Technically, we don't need to MD5 the ID in this case, but it makes
  224. // sense to standardize the format of array indexes for both conditions
  225. $rowid = md5($items['id']);
  226. }
  227. // --------------------------------------------------------------------
  228. // Now that we have our unique "row ID", we'll add our cart items to the master array
  229. // grab quantity if it's already there and add it on
  230. $old_quantity = isset($this->_cart_contents[$rowid]['qty']) ? (int) $this->_cart_contents[$rowid]['qty'] : 0;
  231. // Re-create the entry, just to make sure our index contains only the data from this submission
  232. $items['rowid'] = $rowid;
  233. $items['qty'] += $old_quantity;
  234. $this->_cart_contents[$rowid] = $items;
  235. return $rowid;
  236. }
  237. // --------------------------------------------------------------------
  238. /**
  239. * Update the cart
  240. *
  241. * This function permits the quantity of a given item to be changed.
  242. * Typically it is called from the "view cart" page if a user makes
  243. * changes to the quantity before checkout. That array must contain the
  244. * product ID and quantity for each item.
  245. *
  246. * @param array
  247. * @return bool
  248. */
  249. public function update($items = array())
  250. {
  251. // Was any cart data passed?
  252. if ( ! is_array($items) OR count($items) === 0)
  253. {
  254. return FALSE;
  255. }
  256. // You can either update a single product using a one-dimensional array,
  257. // or multiple products using a multi-dimensional one. The way we
  258. // determine the array type is by looking for a required array key named "rowid".
  259. // If it's not found we assume it's a multi-dimensional array
  260. $save_cart = FALSE;
  261. if (isset($items['rowid']))
  262. {
  263. if ($this->_update($items) === TRUE)
  264. {
  265. $save_cart = TRUE;
  266. }
  267. }
  268. else
  269. {
  270. foreach ($items as $val)
  271. {
  272. if (is_array($val) && isset($val['rowid']))
  273. {
  274. if ($this->_update($val) === TRUE)
  275. {
  276. $save_cart = TRUE;
  277. }
  278. }
  279. }
  280. }
  281. // Save the cart data if the insert was successful
  282. if ($save_cart === TRUE)
  283. {
  284. $this->_save_cart();
  285. return TRUE;
  286. }
  287. return FALSE;
  288. }
  289. // --------------------------------------------------------------------
  290. /**
  291. * Update the cart
  292. *
  293. * This function permits changing item properties.
  294. * Typically it is called from the "view cart" page if a user makes
  295. * changes to the quantity before checkout. That array must contain the
  296. * rowid and quantity for each item.
  297. *
  298. * @param array
  299. * @return bool
  300. */
  301. protected function _update($items = array())
  302. {
  303. // Without these array indexes there is nothing we can do
  304. if ( ! isset($items['rowid'], $this->_cart_contents[$items['rowid']]))
  305. {
  306. return FALSE;
  307. }
  308. // Prep the quantity
  309. if (isset($items['qty']))
  310. {
  311. $items['qty'] = (float) $items['qty'];
  312. // Is the quantity zero? If so we will remove the item from the cart.
  313. // If the quantity is greater than zero we are updating
  314. if ($items['qty'] == 0)
  315. {
  316. unset($this->_cart_contents[$items['rowid']]);
  317. return TRUE;
  318. }
  319. }
  320. // find updatable keys
  321. $keys = array_intersect(array_keys($this->_cart_contents[$items['rowid']]), array_keys($items));
  322. // if a price was passed, make sure it contains valid data
  323. if (isset($items['price']))
  324. {
  325. $items['price'] = (float) $items['price'];
  326. }
  327. // product id & name shouldn't be changed
  328. foreach (array_diff($keys, array('id', 'name')) as $key)
  329. {
  330. $this->_cart_contents[$items['rowid']][$key] = $items[$key];
  331. }
  332. return TRUE;
  333. }
  334. // --------------------------------------------------------------------
  335. /**
  336. * Save the cart array to the session DB
  337. *
  338. * @return bool
  339. */
  340. protected function _save_cart()
  341. {
  342. // Let's add up the individual prices and set the cart sub-total
  343. $this->_cart_contents['total_items'] = $this->_cart_contents['cart_total'] = 0;
  344. foreach ($this->_cart_contents as $key => $val)
  345. {
  346. // We make sure the array contains the proper indexes
  347. if ( ! is_array($val) OR ! isset($val['price'], $val['qty']))
  348. {
  349. continue;
  350. }
  351. $this->_cart_contents['cart_total'] += ($val['price'] * $val['qty']);
  352. $this->_cart_contents['total_items'] += $val['qty'];
  353. $this->_cart_contents[$key]['subtotal'] = ($this->_cart_contents[$key]['price'] * $this->_cart_contents[$key]['qty']);
  354. }
  355. // Is our cart empty? If so we delete it from the session
  356. if (count($this->_cart_contents) <= 2)
  357. {
  358. $this->CI->session->unset_userdata('cart_contents');
  359. // Nothing more to do... coffee time!
  360. return FALSE;
  361. }
  362. // If we made it this far it means that our cart has data.
  363. // Let's pass it to the Session class so it can be stored
  364. $this->CI->session->set_userdata(array('cart_contents' => $this->_cart_contents));
  365. // Woot!
  366. return TRUE;
  367. }
  368. // --------------------------------------------------------------------
  369. /**
  370. * Cart Total
  371. *
  372. * @return int
  373. */
  374. public function total()
  375. {
  376. return $this->_cart_contents['cart_total'];
  377. }
  378. // --------------------------------------------------------------------
  379. /**
  380. * Remove Item
  381. *
  382. * Removes an item from the cart
  383. *
  384. * @param int
  385. * @return bool
  386. */
  387. public function remove($rowid)
  388. {
  389. // unset & save
  390. unset($this->_cart_contents[$rowid]);
  391. $this->_save_cart();
  392. return TRUE;
  393. }
  394. // --------------------------------------------------------------------
  395. /**
  396. * Total Items
  397. *
  398. * Returns the total item count
  399. *
  400. * @return int
  401. */
  402. public function total_items()
  403. {
  404. return $this->_cart_contents['total_items'];
  405. }
  406. // --------------------------------------------------------------------
  407. /**
  408. * Cart Contents
  409. *
  410. * Returns the entire cart array
  411. *
  412. * @param bool
  413. * @return array
  414. */
  415. public function contents($newest_first = FALSE)
  416. {
  417. // do we want the newest first?
  418. $cart = ($newest_first) ? array_reverse($this->_cart_contents) : $this->_cart_contents;
  419. // Remove these so they don't create a problem when showing the cart table
  420. unset($cart['total_items']);
  421. unset($cart['cart_total']);
  422. return $cart;
  423. }
  424. // --------------------------------------------------------------------
  425. /**
  426. * Get cart item
  427. *
  428. * Returns the details of a specific item in the cart
  429. *
  430. * @param string $row_id
  431. * @return array
  432. */
  433. public function get_item($row_id)
  434. {
  435. return (in_array($row_id, array('total_items', 'cart_total'), TRUE) OR ! isset($this->_cart_contents[$row_id]))
  436. ? FALSE
  437. : $this->_cart_contents[$row_id];
  438. }
  439. // --------------------------------------------------------------------
  440. /**
  441. * Has options
  442. *
  443. * Returns TRUE if the rowid passed to this function correlates to an item
  444. * that has options associated with it.
  445. *
  446. * @param string $row_id = ''
  447. * @return bool
  448. */
  449. public function has_options($row_id = '')
  450. {
  451. return (isset($this->_cart_contents[$row_id]['options']) && count($this->_cart_contents[$row_id]['options']) !== 0);
  452. }
  453. // --------------------------------------------------------------------
  454. /**
  455. * Product options
  456. *
  457. * Returns the an array of options, for a particular product row ID
  458. *
  459. * @param string $row_id = ''
  460. * @return array
  461. */
  462. public function product_options($row_id = '')
  463. {
  464. return isset($this->_cart_contents[$row_id]['options']) ? $this->_cart_contents[$row_id]['options'] : array();
  465. }
  466. // --------------------------------------------------------------------
  467. /**
  468. * Format Number
  469. *
  470. * Returns the supplied number with commas and a decimal point.
  471. *
  472. * @param float
  473. * @return string
  474. */
  475. public function format_number($n = '')
  476. {
  477. return ($n === '') ? '' : number_format( (float) $n, 2, '.', ',');
  478. }
  479. // --------------------------------------------------------------------
  480. /**
  481. * Destroy the cart
  482. *
  483. * Empties the cart and kills the session
  484. *
  485. * @return void
  486. */
  487. public function destroy()
  488. {
  489. $this->_cart_contents = array('cart_total' => 0, 'total_items' => 0);
  490. $this->CI->session->unset_userdata('cart_contents');
  491. }
  492. }