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