├── tests ├── .gitkeep ├── helpers │ ├── configMock.php │ ├── configMockOtherFormat.php │ ├── SessionMock.php │ └── MockProduct.php ├── ItemTestOtherFormat.php ├── CartTestMultipleInstances.php ├── ItemTest.php ├── CartTestOtherFormat.php ├── CartTestEvents.php ├── CartTest.php └── CartConditionsTest.php ├── .gitignore ├── src └── Darryldecode │ └── Cart │ ├── CartCollection.php │ ├── Exceptions │ ├── UnknownModelException.php │ ├── InvalidItemException.php │ └── InvalidConditionException.php │ ├── Validators │ ├── CartItemValidator.php │ ├── CartConditionValidator.php │ └── Validator.php │ ├── Facades │ └── CartFacade.php │ ├── CartConditionCollection.php │ ├── ItemAttributeCollection.php │ ├── config │ └── config.php │ ├── CartServiceProvider.php │ ├── Helpers │ └── Helpers.php │ ├── ItemCollection.php │ ├── CartCondition.php │ └── Cart.php ├── .travis.yml ├── phpunit.xml ├── composer.json └── README.md /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea 6 | .phpunit.result.cache -------------------------------------------------------------------------------- /tests/helpers/configMock.php: -------------------------------------------------------------------------------- 1 | false, 5 | 'decimals' => 2, 6 | 'dec_point' => '.', 7 | 'thousands_sep' => ',', 8 | ]; -------------------------------------------------------------------------------- /src/Darryldecode/Cart/CartCollection.php: -------------------------------------------------------------------------------- 1 | true, 5 | 'decimals' => 3, 6 | 'dec_point' => ',', 7 | 'thousands_sep' => '.', 8 | ]; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | 7 | before_script: 8 | - travis_retry composer self-update 9 | - travis_retry composer install --prefer-source --no-interaction --dev 10 | 11 | script: phpunit 12 | -------------------------------------------------------------------------------- /src/Darryldecode/Cart/Validators/CartItemValidator.php: -------------------------------------------------------------------------------- 1 | has($name) ) return $this->get($name); 17 | return null; 18 | } 19 | } -------------------------------------------------------------------------------- /tests/helpers/SessionMock.php: -------------------------------------------------------------------------------- 1 | session[$key]); 16 | } 17 | 18 | public function get($key) 19 | { 20 | return (isset($this->session[$key])) ? $this->session[$key] : null; 21 | } 22 | 23 | public function put($key, $value) 24 | { 25 | $this->session[$key] = $value; 26 | } 27 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Darryldecode/Cart/config/config.php: -------------------------------------------------------------------------------- 1 | env('SHOPPING_FORMAT_VALUES', false), 12 | 13 | 'decimals' => env('SHOPPING_DECIMALS', 0), 14 | 15 | 'dec_point' => env('SHOPPING_DEC_POINT', '.'), 16 | 17 | 'thousands_sep' => env('SHOPPING_THOUSANDS_SEP', ','), 18 | 19 | /* 20 | * --------------------------------------------------------------- 21 | * persistence 22 | * --------------------------------------------------------------- 23 | * 24 | * the configuration for persisting cart 25 | */ 26 | 'storage' => null, 27 | 28 | /* 29 | * --------------------------------------------------------------- 30 | * events 31 | * --------------------------------------------------------------- 32 | * 33 | * the configuration for cart events 34 | */ 35 | 'events' => null, 36 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saleem189/cart", 3 | "description": "Laravel 5 Shopping cart", 4 | "keywords": ["laravel", "shopping cart", "cart"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Darryl Fernandez", 9 | "email": "engrdarrylfernandez@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.3", 14 | "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 15 | "illuminate/validation": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 16 | "illuminate/translation": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0" 17 | }, 18 | "require-dev": { 19 | "mockery/mockery": "^1.4.2", 20 | "phpunit/phpunit": "^9.0", 21 | "symfony/var-dumper": "5.1.5.*@dev" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Darryldecode\\": "src/Darryldecode" 26 | } 27 | }, 28 | "minimum-stability": "dev", 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Darryldecode\\Cart\\CartServiceProvider" 33 | ], 34 | "aliases": { 35 | "Cart": "Darryldecode\\Cart\\Facades\\CartFacade" 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/ItemTestOtherFormat.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 26 | 27 | $this->cart = new Cart( 28 | new SessionMock(), 29 | $events, 30 | 'shopping', 31 | 'SAMPLESESSIONKEY', 32 | require(__DIR__.'/helpers/configMockOtherFormat.php') 33 | ); 34 | } 35 | 36 | public function tearDown(): void 37 | { 38 | m::close(); 39 | } 40 | 41 | public function test_item_get_sum_price_using_property() 42 | { 43 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 44 | 45 | $item = $this->cart->get(455); 46 | 47 | $this->assertEquals('201,980', $item->getPriceSum(), 'Item summed price should be 201.98'); 48 | } 49 | 50 | public function test_item_get_sum_price_using_array_style() 51 | { 52 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 53 | 54 | $item = $this->cart->get(455); 55 | 56 | $this->assertEquals('201,980', $item->getPriceSum(), 'Item summed price should be 201.98'); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Darryldecode/Cart/Validators/Validator.php: -------------------------------------------------------------------------------- 1 | $method(); 41 | 42 | case 1: 43 | return $instance->$method($args[0]); 44 | 45 | case 2: 46 | return $instance->$method($args[0], $args[1]); 47 | 48 | case 3: 49 | return $instance->$method($args[0], $args[1], $args[2]); 50 | 51 | case 4: 52 | return $instance->$method($args[0], $args[1], $args[2], $args[3]); 53 | 54 | default: 55 | return call_user_func_array(array($instance, $method), $args); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /tests/helpers/MockProduct.php: -------------------------------------------------------------------------------- 1 | id = $id; 38 | $this->name = $name; 39 | $this->price = $price; 40 | $this->weight = $weight; 41 | } 42 | 43 | /** 44 | * Get the identifier of the item. 45 | * 46 | * @return int|string 47 | */ 48 | public function getIdentifier($options = null) 49 | { 50 | return $this->id; 51 | } 52 | 53 | /** 54 | * Get the description or title of the item. 55 | * 56 | * @return string 57 | */ 58 | public function getDescription($options = null) 59 | { 60 | return $this->name; 61 | } 62 | 63 | /** 64 | * Get the price of the item. 65 | * 66 | * @return float 67 | */ 68 | public function getPrice($options = null) 69 | { 70 | return $this->price; 71 | } 72 | 73 | /** 74 | * Get the price of the item. 75 | * 76 | * @return float 77 | */ 78 | public function getWeight($options = null) 79 | { 80 | return $this->weight; 81 | } 82 | 83 | public function find($id) 84 | { 85 | return $this; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Darryldecode/Cart/CartServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__.'/config/config.php' => config_path('shopping_cart.php'), 22 | ], 'config'); 23 | } 24 | } 25 | 26 | /** 27 | * Register the service provider. 28 | * 29 | * @return void 30 | */ 31 | public function register() 32 | { 33 | $this->mergeConfigFrom(__DIR__.'/config/config.php', 'shopping_cart'); 34 | 35 | $this->app->singleton('cart', function($app) 36 | { 37 | $storageClass = config('shopping_cart.storage'); 38 | $eventsClass = config('shopping_cart.events'); 39 | 40 | $storage = $storageClass ? new $storageClass() : $app['session']; 41 | $events = $eventsClass ? new $eventsClass() : $app['events']; 42 | $instanceName = 'cart'; 43 | 44 | // default session or cart identifier. This will be overridden when calling Cart::session($sessionKey)->add() etc.. 45 | // like when adding a cart for a specific user name. Session Key can be string or maybe a unique identifier to bind a cart 46 | // to a specific user, this can also be a user ID 47 | $session_key = '4yTlTDKu3oJOfzD'; 48 | 49 | return new Cart( 50 | $storage, 51 | $events, 52 | $instanceName, 53 | $session_key, 54 | config('shopping_cart') 55 | ); 56 | }); 57 | } 58 | 59 | /** 60 | * Get the services provided by the provider. 61 | * 62 | * @return array 63 | */ 64 | public function provides() 65 | { 66 | return array(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Darryldecode/Cart/Helpers/Helpers.php: -------------------------------------------------------------------------------- 1 | $v) 42 | { 43 | if (is_array($v)) 44 | { 45 | return true; 46 | } 47 | else 48 | { 49 | return false; 50 | } 51 | } 52 | 53 | } 54 | } 55 | 56 | /** 57 | * check if variable is set and has value, return a default value 58 | * 59 | * @param $var 60 | * @param bool|mixed $default 61 | * @return bool|mixed 62 | */ 63 | public static function issetAndHasValueOrAssignDefault(&$var, $default = false) 64 | { 65 | if( (isset($var)) && ($var!='') ) return $var; 66 | 67 | return $default; 68 | } 69 | 70 | public static function formatValue($value, $format_numbers, $config) 71 | { 72 | if($format_numbers && $config['format_numbers']) { 73 | return number_format($value, $config['decimals'], $config['dec_point'], $config['thousands_sep']); 74 | } else { 75 | return $value; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /tests/CartTestMultipleInstances.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 30 | 31 | $this->cart1 = new Cart( 32 | new SessionMock(), 33 | $events, 34 | 'shopping', 35 | 'uniquesessionkey123', 36 | require(__DIR__.'/helpers/configMock.php') 37 | ); 38 | 39 | $this->cart2 = new Cart( 40 | new SessionMock(), 41 | $events, 42 | 'wishlist', 43 | 'uniquesessionkey456', 44 | require(__DIR__.'/helpers/configMock.php') 45 | ); 46 | } 47 | 48 | public function tearDown(): void 49 | { 50 | m::close(); 51 | } 52 | 53 | public function test_cart_multiple_instances() 54 | { 55 | // add 3 items on cart 1 56 | $itemsForCart1 = array( 57 | array( 58 | 'id' => 456, 59 | 'name' => 'Sample Item 1', 60 | 'price' => 67.99, 61 | 'quantity' => 4, 62 | 'attributes' => array() 63 | ), 64 | array( 65 | 'id' => 568, 66 | 'name' => 'Sample Item 2', 67 | 'price' => 69.25, 68 | 'quantity' => 4, 69 | 'attributes' => array() 70 | ), 71 | array( 72 | 'id' => 856, 73 | 'name' => 'Sample Item 3', 74 | 'price' => 50.25, 75 | 'quantity' => 4, 76 | 'attributes' => array() 77 | ), 78 | ); 79 | 80 | $this->cart1->add($itemsForCart1); 81 | 82 | $this->assertFalse($this->cart1->isEmpty(), 'Cart should not be empty'); 83 | $this->assertCount(3, $this->cart1->getContent()->toArray(), 'Cart should have 3 items'); 84 | $this->assertEquals('shopping', $this->cart1->getInstanceName(), 'Cart 1 should have instance name of "shopping"'); 85 | 86 | // add 1 item on cart 2 87 | $itemsForCart2 = array( 88 | array( 89 | 'id' => 456, 90 | 'name' => 'Sample Item 1', 91 | 'price' => 67.99, 92 | 'quantity' => 4, 93 | 'attributes' => array() 94 | ), 95 | ); 96 | 97 | $this->cart2->add($itemsForCart2); 98 | 99 | $this->assertFalse($this->cart2->isEmpty(), 'Cart should not be empty'); 100 | $this->assertCount(1, $this->cart2->getContent()->toArray(), 'Cart should have 3 items'); 101 | $this->assertEquals('wishlist', $this->cart2->getInstanceName(), 'Cart 2 should have instance name of "wishlist"'); 102 | } 103 | } -------------------------------------------------------------------------------- /src/Darryldecode/Cart/ItemCollection.php: -------------------------------------------------------------------------------- 1 | config = $config; 33 | } 34 | 35 | /** 36 | * get the sum of price 37 | * 38 | * @return mixed|null 39 | */ 40 | public function getPriceSum() 41 | { 42 | return Helpers::formatValue($this->price * $this->quantity, $this->config['format_numbers'], $this->config); 43 | } 44 | 45 | public function __get($name) 46 | { 47 | if ($this->has($name) || $name == 'model') { 48 | return !is_null($this->get($name)) ? $this->get($name) : $this->getAssociatedModel(); 49 | } 50 | return null; 51 | } 52 | 53 | /** 54 | * return the associated model of an item 55 | * 56 | * @return bool 57 | */ 58 | protected function getAssociatedModel() 59 | { 60 | if (!$this->has('associatedModel')) { 61 | return null; 62 | } 63 | 64 | $associatedModel = $this->get('associatedModel'); 65 | 66 | return with(new $associatedModel())->find($this->get('id')); 67 | } 68 | 69 | /** 70 | * check if item has conditions 71 | * 72 | * @return bool 73 | */ 74 | public function hasConditions() 75 | { 76 | if (!isset($this['conditions'])) return false; 77 | if (is_array($this['conditions'])) { 78 | return count($this['conditions']) > 0; 79 | } 80 | $conditionInstance = "Darryldecode\\Cart\\CartCondition"; 81 | if ($this['conditions'] instanceof $conditionInstance) return true; 82 | 83 | return false; 84 | } 85 | 86 | /** 87 | * check if item has conditions 88 | * 89 | * @return mixed|null 90 | */ 91 | public function getConditions() 92 | { 93 | if (!$this->hasConditions()) return []; 94 | return $this['conditions']; 95 | } 96 | 97 | /** 98 | * get the single price in which conditions are already applied 99 | * @param bool $formatted 100 | * @return mixed|null 101 | */ 102 | public function getPriceWithConditions($formatted = true) 103 | { 104 | $originalPrice = $this->price; 105 | $newPrice = 0.00; 106 | $processed = 0; 107 | 108 | if ($this->hasConditions()) { 109 | if (is_array($this->conditions)) { 110 | foreach ($this->conditions as $condition) { 111 | ($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice; 112 | $newPrice = $condition->applyCondition($toBeCalculated); 113 | $processed++; 114 | } 115 | } else { 116 | $newPrice = $this['conditions']->applyCondition($originalPrice); 117 | } 118 | 119 | return Helpers::formatValue($newPrice, $formatted, $this->config); 120 | } 121 | return Helpers::formatValue($originalPrice, $formatted, $this->config); 122 | } 123 | 124 | /** 125 | * get the sum of price in which conditions are already applied 126 | * @param bool $formatted 127 | * @return mixed|null 128 | */ 129 | public function getPriceSumWithConditions($formatted = true) 130 | { 131 | return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/ItemTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 28 | 29 | $this->cart = new Cart( 30 | new SessionMock(), 31 | $events, 32 | 'shopping', 33 | 'SAMPLESESSIONKEY', 34 | require(__DIR__ . '/helpers/configMock.php') 35 | ); 36 | } 37 | 38 | public function tearDown(): void 39 | { 40 | m::close(); 41 | } 42 | 43 | public function test_item_get_sum_price_using_property() 44 | { 45 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 46 | 47 | $item = $this->cart->get(455); 48 | 49 | $this->assertEquals(201.98, $item->getPriceSum(), 'Item summed price should be 201.98'); 50 | } 51 | 52 | public function test_item_get_sum_price_using_array_style() 53 | { 54 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 55 | 56 | $item = $this->cart->get(455); 57 | 58 | $this->assertEquals(201.98, $item->getPriceSum(), 'Item summed price should be 201.98'); 59 | } 60 | 61 | public function test_item_get_conditions_empty() 62 | { 63 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 64 | 65 | $item = $this->cart->get(455); 66 | 67 | $this->assertEmpty($item->getConditions(), 'Item should have no conditions'); 68 | } 69 | 70 | public function test_item_get_conditions_with_conditions() 71 | { 72 | $itemCondition1 = new \Darryldecode\Cart\CartCondition(array( 73 | 'name' => 'SALE 5%', 74 | 'type' => 'sale', 75 | 'target' => 'item', 76 | 'value' => '-5%', 77 | )); 78 | 79 | $itemCondition2 = new CartCondition(array( 80 | 'name' => 'Item Gift Pack 25.00', 81 | 'type' => 'promo', 82 | 'target' => 'item', 83 | 'value' => '-25', 84 | )); 85 | 86 | $this->cart->add(455, 'Sample Item', 100.99, 2, array(), [$itemCondition1, $itemCondition2]); 87 | 88 | $item = $this->cart->get(455); 89 | 90 | $this->assertCount(2, $item->getConditions(), 'Item should have two conditions'); 91 | } 92 | 93 | public function test_item_associate_model() 94 | { 95 | $this->cart->add(455, 'Sample Item', 100.99, 2, array())->associate(MockProduct::class); 96 | 97 | $item = $this->cart->get(455); 98 | 99 | $this->assertEquals(MockProduct::class, $item->associatedModel, 'Item assocaited model should be ' . MockProduct::class); 100 | } 101 | 102 | public function test_it_will_throw_an_exception_when_a_non_existing_model_is_being_associated() 103 | { 104 | $this->expectException(\Darryldecode\Cart\Exceptions\UnknownModelException::class); 105 | $this->expectExceptionMessage('The supplied model SomeModel does not exist.'); 106 | 107 | $this->cart->add(1, 'Test item', 1, 10.00)->associate('SomeModel'); 108 | } 109 | 110 | public function test_item_get_model() 111 | { 112 | $this->cart->add(455, 'Sample Item', 100.99, 2, array())->associate(MockProduct::class); 113 | 114 | $item = $this->cart->get(455); 115 | 116 | $this->assertInstanceOf(MockProduct::class, $item->model); 117 | $this->assertEquals('Sample Item', $item->model->name); 118 | $this->assertEquals(455, $item->model->id); 119 | } 120 | 121 | public function test_item_get_model_will_return_null_if_it_has_no_model() 122 | { 123 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 124 | 125 | $item = $this->cart->get(455); 126 | 127 | $this->assertEquals(null, $item->model); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/CartTestOtherFormat.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 25 | 26 | $this->cart = new Cart( 27 | new SessionMock(), 28 | $events, 29 | 'shopping', 30 | 'SAMPLESESSIONKEY', 31 | require(__DIR__.'/helpers/configMockOtherFormat.php') 32 | ); 33 | } 34 | 35 | public function tearDown(): void 36 | { 37 | m::close(); 38 | } 39 | 40 | public function test_cart_sub_total() 41 | { 42 | $items = array( 43 | array( 44 | 'id' => 456, 45 | 'name' => 'Sample Item 1', 46 | 'price' => 67.99, 47 | 'quantity' => 1, 48 | 'attributes' => array() 49 | ), 50 | array( 51 | 'id' => 568, 52 | 'name' => 'Sample Item 2', 53 | 'price' => 69.25, 54 | 'quantity' => 1, 55 | 'attributes' => array() 56 | ), 57 | array( 58 | 'id' => 856, 59 | 'name' => 'Sample Item 3', 60 | 'price' => 50.25, 61 | 'quantity' => 1, 62 | 'attributes' => array() 63 | ), 64 | ); 65 | 66 | $this->cart->add($items); 67 | 68 | $this->assertEquals('187,490', $this->cart->getSubTotal(), 'Cart should have sub total of 187,490'); 69 | 70 | // if we remove an item, the sub total should be updated as well 71 | $this->cart->remove(456); 72 | 73 | $this->assertEquals('119,500', $this->cart->getSubTotal(), 'Cart should have sub total of 119,500'); 74 | } 75 | 76 | public function test_sub_total_when_item_quantity_is_updated() 77 | { 78 | $items = array( 79 | array( 80 | 'id' => 456, 81 | 'name' => 'Sample Item 1', 82 | 'price' => 67.99, 83 | 'quantity' => 3, 84 | 'attributes' => array() 85 | ), 86 | array( 87 | 'id' => 568, 88 | 'name' => 'Sample Item 2', 89 | 'price' => 69.25, 90 | 'quantity' => 1, 91 | 'attributes' => array() 92 | ), 93 | ); 94 | 95 | $this->cart->add($items); 96 | 97 | $this->assertEquals('273,220', $this->cart->getSubTotal(), 'Cart should have sub total of 273.22'); 98 | 99 | // when cart's item quantity is updated, the subtotal should be updated as well 100 | $this->cart->update(456, array('quantity' => 2)); 101 | 102 | $this->assertEquals('409,200', $this->cart->getSubTotal(), 'Cart should have sub total of 409.2'); 103 | } 104 | 105 | public function test_sub_total_when_item_quantity_is_updated_by_reduced() 106 | { 107 | $items = array( 108 | array( 109 | 'id' => 456, 110 | 'name' => 'Sample Item 1', 111 | 'price' => 67.99, 112 | 'quantity' => 3, 113 | 'attributes' => array() 114 | ), 115 | array( 116 | 'id' => 568, 117 | 'name' => 'Sample Item 2', 118 | 'price' => 69.25, 119 | 'quantity' => 1, 120 | 'attributes' => array() 121 | ), 122 | ); 123 | 124 | $this->cart->add($items); 125 | 126 | $this->assertEquals('273,220', $this->cart->getSubTotal(), 'Cart should have sub total of 273.22'); 127 | 128 | // when cart's item quantity is updated, the subtotal should be updated as well 129 | $this->cart->update(456, array('quantity' => -1)); 130 | 131 | // get the item to be evaluated 132 | $item = $this->cart->get(456); 133 | 134 | $this->assertEquals(2, $item['quantity'], 'Item quantity of with item ID of 456 should now be reduced to 2'); 135 | $this->assertEquals('205,230', $this->cart->getSubTotal(), 'Cart should have sub total of 205.23'); 136 | } 137 | } -------------------------------------------------------------------------------- /tests/CartTestEvents.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 31 | 32 | $cart = new Cart( 33 | new SessionMock(), 34 | $events, 35 | self::CART_INSTANCE_NAME, 36 | 'SAMPLESESSIONKEY', 37 | require(__DIR__.'/helpers/configMock.php') 38 | ); 39 | 40 | $this->assertTrue(true); 41 | } 42 | 43 | public function test_event_cart_adding() 44 | { 45 | $events = m::mock('Illuminate\Events\Dispatcher'); 46 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 47 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.adding', m::type('array'), true); 48 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.added', m::type('array'), true); 49 | 50 | $cart = new Cart( 51 | new SessionMock(), 52 | $events, 53 | self::CART_INSTANCE_NAME, 54 | 'SAMPLESESSIONKEY', 55 | require(__DIR__.'/helpers/configMock.php') 56 | ); 57 | 58 | $cart->add(455, 'Sample Item', 100.99, 2, array()); 59 | 60 | $this->assertTrue(true); 61 | } 62 | 63 | public function test_event_cart_adding_multiple_times() 64 | { 65 | $events = m::mock('Illuminate\Events\Dispatcher'); 66 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 67 | $events->shouldReceive('dispatch')->times(2)->with(self::CART_INSTANCE_NAME.'.adding', m::type('array'), true); 68 | $events->shouldReceive('dispatch')->times(2)->with(self::CART_INSTANCE_NAME.'.added', m::type('array'), true); 69 | 70 | $cart = new Cart( 71 | new SessionMock(), 72 | $events, 73 | self::CART_INSTANCE_NAME, 74 | 'SAMPLESESSIONKEY', 75 | require(__DIR__.'/helpers/configMock.php') 76 | ); 77 | 78 | $cart->add(455, 'Sample Item 1', 100.99, 2, array()); 79 | $cart->add(562, 'Sample Item 2', 100.99, 2, array()); 80 | 81 | $this->assertTrue(true); 82 | } 83 | 84 | public function test_event_cart_adding_multiple_times_scenario_two() 85 | { 86 | $events = m::mock('Illuminate\Events\Dispatcher'); 87 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 88 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.adding', m::type('array'), true); 89 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.added', m::type('array'), true); 90 | 91 | $items = array( 92 | array( 93 | 'id' => 456, 94 | 'name' => 'Sample Item 1', 95 | 'price' => 67.99, 96 | 'quantity' => 4, 97 | 'attributes' => array() 98 | ), 99 | array( 100 | 'id' => 568, 101 | 'name' => 'Sample Item 2', 102 | 'price' => 69.25, 103 | 'quantity' => 4, 104 | 'attributes' => array() 105 | ), 106 | array( 107 | 'id' => 856, 108 | 'name' => 'Sample Item 3', 109 | 'price' => 50.25, 110 | 'quantity' => 4, 111 | 'attributes' => array() 112 | ), 113 | ); 114 | 115 | $cart = new Cart( 116 | new SessionMock(), 117 | $events, 118 | self::CART_INSTANCE_NAME, 119 | 'SAMPLESESSIONKEY', 120 | require(__DIR__.'/helpers/configMock.php') 121 | ); 122 | 123 | $cart->add($items); 124 | 125 | $this->assertTrue(true); 126 | } 127 | 128 | public function test_event_cart_remove_item() 129 | { 130 | $events = m::mock('Illuminate\Events\Dispatcher'); 131 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 132 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.adding', m::type('array'), true); 133 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.added', m::type('array'), true); 134 | $events->shouldReceive('dispatch')->times(1)->with(self::CART_INSTANCE_NAME.'.removing', m::type('array'), true); 135 | $events->shouldReceive('dispatch')->times(1)->with(self::CART_INSTANCE_NAME.'.removed', m::type('array'), true); 136 | 137 | $items = array( 138 | array( 139 | 'id' => 456, 140 | 'name' => 'Sample Item 1', 141 | 'price' => 67.99, 142 | 'quantity' => 4, 143 | 'attributes' => array() 144 | ), 145 | array( 146 | 'id' => 568, 147 | 'name' => 'Sample Item 2', 148 | 'price' => 69.25, 149 | 'quantity' => 4, 150 | 'attributes' => array() 151 | ), 152 | array( 153 | 'id' => 856, 154 | 'name' => 'Sample Item 3', 155 | 'price' => 50.25, 156 | 'quantity' => 4, 157 | 'attributes' => array() 158 | ), 159 | ); 160 | 161 | $cart = new Cart( 162 | new SessionMock(), 163 | $events, 164 | self::CART_INSTANCE_NAME, 165 | 'SAMPLESESSIONKEY', 166 | require(__DIR__.'/helpers/configMock.php') 167 | ); 168 | 169 | $cart->add($items); 170 | 171 | $cart->remove(456); 172 | 173 | $this->assertTrue(true); 174 | } 175 | 176 | public function test_event_cart_clear() 177 | { 178 | $events = m::mock('Illuminate\Events\Dispatcher'); 179 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.created', m::type('array'), true); 180 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.adding', m::type('array'), true); 181 | $events->shouldReceive('dispatch')->times(3)->with(self::CART_INSTANCE_NAME.'.added', m::type('array'), true); 182 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.clearing', m::type('array'), true); 183 | $events->shouldReceive('dispatch')->once()->with(self::CART_INSTANCE_NAME.'.cleared', m::type('array'), true); 184 | 185 | $items = array( 186 | array( 187 | 'id' => 456, 188 | 'name' => 'Sample Item 1', 189 | 'price' => 67.99, 190 | 'quantity' => 4, 191 | 'attributes' => array() 192 | ), 193 | array( 194 | 'id' => 568, 195 | 'name' => 'Sample Item 2', 196 | 'price' => 69.25, 197 | 'quantity' => 4, 198 | 'attributes' => array() 199 | ), 200 | array( 201 | 'id' => 856, 202 | 'name' => 'Sample Item 3', 203 | 'price' => 50.25, 204 | 'quantity' => 4, 205 | 'attributes' => array() 206 | ), 207 | ); 208 | 209 | $cart = new Cart( 210 | new SessionMock(), 211 | $events, 212 | self::CART_INSTANCE_NAME, 213 | 'SAMPLESESSIONKEY', 214 | require(__DIR__.'/helpers/configMock.php') 215 | ); 216 | 217 | $cart->add($items); 218 | 219 | $cart->clear(); 220 | 221 | $this->assertTrue(true); 222 | } 223 | } -------------------------------------------------------------------------------- /src/Darryldecode/Cart/CartCondition.php: -------------------------------------------------------------------------------- 1 | args = $args; 34 | 35 | if( Helpers::isMultiArray($args) ) 36 | { 37 | Throw new InvalidConditionException('Multi dimensional array is not supported.'); 38 | } 39 | else 40 | { 41 | $this->validate($this->args); 42 | } 43 | } 44 | 45 | /** 46 | * the target of where the condition is applied. 47 | * NOTE: On conditions added to per item bases, target is not needed. 48 | * 49 | * @return mixed 50 | */ 51 | public function getTarget() 52 | { 53 | return (isset($this->args['target'])) ? $this->args['target'] : ''; 54 | } 55 | 56 | /** 57 | * the name of the condition 58 | * 59 | * @return mixed 60 | */ 61 | public function getName() 62 | { 63 | return $this->args['name']; 64 | } 65 | 66 | /** 67 | * the type of the condition 68 | * 69 | * @return mixed 70 | */ 71 | public function getType() 72 | { 73 | return $this->args['type']; 74 | } 75 | 76 | /** 77 | * get the additional attributes of a condition 78 | * 79 | * @return array 80 | */ 81 | public function getAttributes() 82 | { 83 | return (isset($this->args['attributes'])) ? $this->args['attributes'] : array(); 84 | } 85 | 86 | /** 87 | * the value of this the condition 88 | * 89 | * @return mixed 90 | */ 91 | public function getValue() 92 | { 93 | return $this->args['value']; 94 | } 95 | 96 | /** 97 | * Set the order to apply this condition. If no argument order is applied we return 0 as 98 | * indicator that no assignment has been made 99 | * @param int $order 100 | * @return Integer 101 | */ 102 | public function setOrder($order = 1) 103 | { 104 | $this->args['order'] = $order; 105 | } 106 | 107 | /** 108 | * the order to apply this condition. If no argument order is applied we return 0 as 109 | * indicator that no assignment has been made 110 | * 111 | * @return Integer 112 | */ 113 | public function getOrder() 114 | { 115 | return isset($this->args['order']) && is_numeric($this->args['order']) ? (int)$this->args['order'] : 0; 116 | } 117 | 118 | /** 119 | * apply condition to total or subtotal 120 | * 121 | * @param $totalOrSubTotalOrPrice 122 | * @return float 123 | */ 124 | public function applyCondition($totalOrSubTotalOrPrice) 125 | { 126 | return $this->apply($totalOrSubTotalOrPrice, $this->getValue()); 127 | } 128 | 129 | /** 130 | * get the calculated value of this condition supplied by the subtotal|price 131 | * 132 | * @param $totalOrSubTotalOrPrice 133 | * @return mixed 134 | */ 135 | public function getCalculatedValue($totalOrSubTotalOrPrice) 136 | { 137 | $this->apply($totalOrSubTotalOrPrice, $this->getValue()); 138 | 139 | return $this->parsedRawValue; 140 | } 141 | 142 | /** 143 | * apply condition 144 | * 145 | * @param $totalOrSubTotalOrPrice 146 | * @param $conditionValue 147 | * @return float 148 | */ 149 | protected function apply($totalOrSubTotalOrPrice, $conditionValue) 150 | { 151 | // if value has a percentage sign on it, we will get first 152 | // its percentage then we will evaluate again if the value 153 | // has a minus or plus sign so we can decide what to do with the 154 | // percentage, whether to add or subtract it to the total/subtotal/price 155 | // if we can't find any plus/minus sign, we will assume it as plus sign 156 | if( $this->valueIsPercentage($conditionValue) ) 157 | { 158 | if( $this->valueIsToBeSubtracted($conditionValue) ) 159 | { 160 | $value = Helpers::normalizePrice( $this->cleanValue($conditionValue) ); 161 | 162 | $this->parsedRawValue = $totalOrSubTotalOrPrice * ($value / 100); 163 | 164 | $result = floatval($totalOrSubTotalOrPrice - $this->parsedRawValue); 165 | } 166 | else if ( $this->valueIsToBeAdded($conditionValue) ) 167 | { 168 | $value = Helpers::normalizePrice( $this->cleanValue($conditionValue) ); 169 | 170 | $this->parsedRawValue = $totalOrSubTotalOrPrice * ($value / 100); 171 | 172 | $result = floatval($totalOrSubTotalOrPrice + $this->parsedRawValue); 173 | } 174 | else 175 | { 176 | $value = Helpers::normalizePrice($conditionValue); 177 | 178 | $this->parsedRawValue = $totalOrSubTotalOrPrice * ($value / 100); 179 | 180 | $result = floatval($totalOrSubTotalOrPrice + $this->parsedRawValue); 181 | } 182 | } 183 | 184 | // if the value has no percent sign on it, the operation will not be a percentage 185 | // next is we will check if it has a minus/plus sign so then we can just deduct it to total/subtotal/price 186 | else 187 | { 188 | if( $this->valueIsToBeSubtracted($conditionValue) ) 189 | { 190 | $this->parsedRawValue = Helpers::normalizePrice( $this->cleanValue($conditionValue) ); 191 | 192 | $result = floatval($totalOrSubTotalOrPrice - $this->parsedRawValue); 193 | } 194 | else if ( $this->valueIsToBeAdded($conditionValue) ) 195 | { 196 | $this->parsedRawValue = Helpers::normalizePrice( $this->cleanValue($conditionValue) ); 197 | 198 | $result = floatval($totalOrSubTotalOrPrice + $this->parsedRawValue); 199 | } 200 | else 201 | { 202 | $this->parsedRawValue = Helpers::normalizePrice($conditionValue); 203 | 204 | $result = floatval($totalOrSubTotalOrPrice + $this->parsedRawValue); 205 | } 206 | } 207 | 208 | // Do not allow items with negative prices. 209 | return $result < 0 ? 0.00 : $result; 210 | } 211 | 212 | /** 213 | * check if value is a percentage 214 | * 215 | * @param $value 216 | * @return bool 217 | */ 218 | protected function valueIsPercentage($value) 219 | { 220 | return (preg_match('/%/', $value) == 1); 221 | } 222 | 223 | /** 224 | * check if value is a subtract 225 | * 226 | * @param $value 227 | * @return bool 228 | */ 229 | protected function valueIsToBeSubtracted($value) 230 | { 231 | return (preg_match('/\-/', $value) == 1); 232 | } 233 | 234 | /** 235 | * check if value is to be added 236 | * 237 | * @param $value 238 | * @return bool 239 | */ 240 | protected function valueIsToBeAdded($value) 241 | { 242 | return (preg_match('/\+/', $value) == 1); 243 | } 244 | 245 | /** 246 | * removes some arithmetic signs (%,+,-) only 247 | * 248 | * @param $value 249 | * @return mixed 250 | */ 251 | protected function cleanValue($value) 252 | { 253 | return str_replace(array('%','-','+'),'',$value); 254 | } 255 | 256 | /** 257 | * validates condition arguments 258 | * 259 | * @param $args 260 | * @throws InvalidConditionException 261 | */ 262 | protected function validate($args) 263 | { 264 | $rules = array( 265 | 'name' => 'required', 266 | 'type' => 'required', 267 | 'value' => 'required', 268 | ); 269 | 270 | $validator = CartConditionValidator::make($args, $rules); 271 | 272 | if( $validator->fails() ) 273 | { 274 | throw new InvalidConditionException($validator->messages()->first()); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tests/CartTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 28 | 29 | $this->cart = new Cart( 30 | new SessionMock(), 31 | $events, 32 | 'shopping', 33 | 'SAMPLESESSIONKEY', 34 | require(__DIR__ . '/helpers/configMock.php') 35 | ); 36 | } 37 | 38 | public function tearDown(): void 39 | { 40 | m::close(); 41 | } 42 | 43 | public function test_cart_can_add_item() 44 | { 45 | $this->cart->add(455, 'Sample Item', 100.99, 2, array()); 46 | 47 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 48 | $this->assertEquals(1, $this->cart->getContent()->count(), 'Cart content should be 1'); 49 | $this->assertEquals(455, $this->cart->getContent()->first()['id'], 'Item added has ID of 455 so first content ID should be 455'); 50 | $this->assertEquals(100.99, $this->cart->getContent()->first()['price'], 'Item added has price of 100.99 so first content price should be 100.99'); 51 | } 52 | 53 | public function test_cart_can_add_items_as_array() 54 | { 55 | $item = array( 56 | 'id' => 456, 57 | 'name' => 'Sample Item', 58 | 'price' => 67.99, 59 | 'quantity' => 4, 60 | 'attributes' => array() 61 | ); 62 | 63 | $this->cart->add($item); 64 | 65 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 66 | $this->assertEquals(1, $this->cart->getContent()->count(), 'Cart should have 1 item on it'); 67 | $this->assertEquals(456, $this->cart->getContent()->first()['id'], 'The first content must have ID of 456'); 68 | $this->assertEquals('Sample Item', $this->cart->getContent()->first()['name'], 'The first content must have name of "Sample Item"'); 69 | } 70 | 71 | public function test_cart_can_add_items_with_multidimensional_array() 72 | { 73 | $items = array( 74 | array( 75 | 'id' => 456, 76 | 'name' => 'Sample Item 1', 77 | 'price' => 67.99, 78 | 'quantity' => 4, 79 | 'attributes' => array() 80 | ), 81 | array( 82 | 'id' => 568, 83 | 'name' => 'Sample Item 2', 84 | 'price' => 69.25, 85 | 'quantity' => 4, 86 | 'attributes' => array() 87 | ), 88 | array( 89 | 'id' => 856, 90 | 'name' => 'Sample Item 3', 91 | 'price' => 50.25, 92 | 'quantity' => 4, 93 | 'attributes' => array() 94 | ), 95 | ); 96 | 97 | $this->cart->add($items); 98 | 99 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 100 | $this->assertCount(3, $this->cart->getContent()->toArray(), 'Cart should have 3 items'); 101 | } 102 | 103 | public function test_cart_can_add_item_without_attributes() 104 | { 105 | $item = array( 106 | 'id' => 456, 107 | 'name' => 'Sample Item 1', 108 | 'price' => 67.99, 109 | 'quantity' => 4 110 | ); 111 | 112 | $this->cart->add($item); 113 | 114 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 115 | } 116 | 117 | public function test_cart_update_with_attribute_then_attributes_should_be_still_instance_of_ItemAttributeCollection() 118 | { 119 | $item = array( 120 | 'id' => 456, 121 | 'name' => 'Sample Item 1', 122 | 'price' => 67.99, 123 | 'quantity' => 4, 124 | 'attributes' => array( 125 | 'product_id' => '145', 126 | 'color' => 'red' 127 | ) 128 | ); 129 | $this->cart->add($item); 130 | 131 | // lets get the attribute and prove first its an instance of 132 | // ItemAttributeCollection 133 | $item = $this->cart->get(456); 134 | 135 | $this->assertInstanceOf('Darryldecode\Cart\ItemAttributeCollection', $item->attributes); 136 | 137 | // now lets update the item with its new attributes 138 | // when we get that item from cart, it should still be an instance of ItemAttributeCollection 139 | $updatedItem = array( 140 | 'attributes' => array( 141 | 'product_id' => '145', 142 | 'color' => 'red' 143 | ) 144 | ); 145 | $this->cart->update(456, $updatedItem); 146 | 147 | $this->assertInstanceOf('Darryldecode\Cart\ItemAttributeCollection', $item->attributes); 148 | } 149 | 150 | public function test_cart_items_attributes() 151 | { 152 | $item = array( 153 | 'id' => 456, 154 | 'name' => 'Sample Item 1', 155 | 'price' => 67.99, 156 | 'quantity' => 4, 157 | 'attributes' => array( 158 | 'size' => 'L', 159 | 'color' => 'blue' 160 | ) 161 | ); 162 | 163 | $this->cart->add($item); 164 | 165 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 166 | $this->assertCount(2, $this->cart->getContent()->first()['attributes'], 'Item\'s attribute should have two'); 167 | $this->assertEquals('L', $this->cart->getContent()->first()->attributes->size, 'Item should have attribute size of L'); 168 | $this->assertEquals('blue', $this->cart->getContent()->first()->attributes->color, 'Item should have attribute color of blue'); 169 | $this->assertTrue($this->cart->get(456)->has('attributes'), 'Item should have attributes'); 170 | $this->assertEquals('L', $this->cart->get(456)->get('attributes')->size); 171 | } 172 | 173 | public function test_cart_update_existing_item() 174 | { 175 | $items = array( 176 | array( 177 | 'id' => 456, 178 | 'name' => 'Sample Item 1', 179 | 'price' => 67.99, 180 | 'quantity' => 3, 181 | 'attributes' => array() 182 | ), 183 | array( 184 | 'id' => 568, 185 | 'name' => 'Sample Item 2', 186 | 'price' => 69.25, 187 | 'quantity' => 1, 188 | 'attributes' => array() 189 | ), 190 | ); 191 | 192 | $this->cart->add($items); 193 | 194 | $itemIdToEvaluate = 456; 195 | 196 | $item = $this->cart->get($itemIdToEvaluate); 197 | $this->assertEquals('Sample Item 1', $item['name'], 'Item name should be "Sample Item 1"'); 198 | $this->assertEquals(67.99, $item['price'], 'Item price should be "67.99"'); 199 | $this->assertEquals(3, $item['quantity'], 'Item quantity should be 3'); 200 | 201 | // when cart's item quantity is updated, the subtotal should be updated as well 202 | $this->cart->update(456, array( 203 | 'name' => 'Renamed', 204 | 'quantity' => 2, 205 | 'price' => 105, 206 | )); 207 | 208 | $item = $this->cart->get($itemIdToEvaluate); 209 | $this->assertEquals('Renamed', $item['name'], 'Item name should be "Renamed"'); 210 | $this->assertEquals(105, $item['price'], 'Item price should be 105'); 211 | $this->assertEquals(5, $item['quantity'], 'Item quantity should be 2'); 212 | } 213 | 214 | public function test_cart_update_existing_item_with_quantity_as_array_and_not_relative() 215 | { 216 | $items = array( 217 | array( 218 | 'id' => 456, 219 | 'name' => 'Sample Item 1', 220 | 'price' => 67.99, 221 | 'quantity' => 3, 222 | 'attributes' => array() 223 | ), 224 | ); 225 | 226 | $this->cart->add($items); 227 | 228 | $itemIdToEvaluate = 456; 229 | $item = $this->cart->get($itemIdToEvaluate); 230 | $this->assertEquals(3, $item['quantity'], 'Item quantity should be 3'); 231 | 232 | // now by default when an update takes place and the quantity attribute 233 | // is present, it will evaluate for arithmetic operation if the quantity 234 | // should be incremented or decremented, we should also allow the quantity 235 | // value to be in array format and provide a field if the quantity should not be 236 | // treated as relative to Item quantity current value 237 | $this->cart->update($itemIdToEvaluate, array('quantity' => array('relative' => false, 'value' => 5))); 238 | 239 | $item = $this->cart->get($itemIdToEvaluate); 240 | $this->assertEquals(5, $item['quantity'], 'Item quantity should be 5'); 241 | } 242 | 243 | public function test_item_price_should_be_normalized_when_added_to_cart() 244 | { 245 | // add a price in a string format should be converted to float 246 | $this->cart->add(455, 'Sample Item', '100.99', 2, array()); 247 | 248 | $this->assertIsFloat($this->cart->getContent()->first()['price'], 'Cart price should be a float'); 249 | } 250 | 251 | public function test_it_removes_an_item_on_cart_by_item_id() 252 | { 253 | $items = array( 254 | array( 255 | 'id' => 456, 256 | 'name' => 'Sample Item 1', 257 | 'price' => 67.99, 258 | 'quantity' => 4, 259 | 'attributes' => array() 260 | ), 261 | array( 262 | 'id' => 568, 263 | 'name' => 'Sample Item 2', 264 | 'price' => 69.25, 265 | 'quantity' => 4, 266 | 'attributes' => array() 267 | ), 268 | array( 269 | 'id' => 856, 270 | 'name' => 'Sample Item 3', 271 | 'price' => 50.25, 272 | 'quantity' => 4, 273 | 'attributes' => array() 274 | ), 275 | ); 276 | 277 | $this->cart->add($items); 278 | 279 | $removeItemId = 456; 280 | 281 | $this->cart->remove($removeItemId); 282 | 283 | $this->assertCount(2, $this->cart->getContent()->toArray(), 'Cart must have 2 items left'); 284 | $this->assertFalse($this->cart->getContent()->has($removeItemId), 'Cart must have not contain the remove item anymore'); 285 | } 286 | 287 | public function test_cart_sub_total() 288 | { 289 | $items = array( 290 | array( 291 | 'id' => 456, 292 | 'name' => 'Sample Item 1', 293 | 'price' => 67.99, 294 | 'quantity' => 1, 295 | 'attributes' => array() 296 | ), 297 | array( 298 | 'id' => 568, 299 | 'name' => 'Sample Item 2', 300 | 'price' => 69.25, 301 | 'quantity' => 1, 302 | 'attributes' => array() 303 | ), 304 | array( 305 | 'id' => 856, 306 | 'name' => 'Sample Item 3', 307 | 'price' => 50.25, 308 | 'quantity' => 1, 309 | 'attributes' => array() 310 | ), 311 | ); 312 | 313 | $this->cart->add($items); 314 | 315 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 316 | 317 | // if we remove an item, the sub total should be updated as well 318 | $this->cart->remove(456); 319 | 320 | $this->assertEquals(119.5, $this->cart->getSubTotal(), 'Cart should have sub total of 119.5'); 321 | } 322 | 323 | public function test_sub_total_when_item_quantity_is_updated() 324 | { 325 | $items = array( 326 | array( 327 | 'id' => 456, 328 | 'name' => 'Sample Item 1', 329 | 'price' => 67.99, 330 | 'quantity' => 3, 331 | 'attributes' => array() 332 | ), 333 | array( 334 | 'id' => 568, 335 | 'name' => 'Sample Item 2', 336 | 'price' => 69.25, 337 | 'quantity' => 1, 338 | 'attributes' => array() 339 | ), 340 | ); 341 | 342 | $this->cart->add($items); 343 | 344 | $this->assertEquals(273.22, $this->cart->getSubTotal(), 'Cart should have sub total of 273.22'); 345 | 346 | // when cart's item quantity is updated, the subtotal should be updated as well 347 | $this->cart->update(456, array('quantity' => 2)); 348 | 349 | $this->assertEquals(409.2, $this->cart->getSubTotal(), 'Cart should have sub total of 409.2'); 350 | } 351 | 352 | public function test_sub_total_when_item_quantity_is_updated_by_reduced() 353 | { 354 | $items = array( 355 | array( 356 | 'id' => 456, 357 | 'name' => 'Sample Item 1', 358 | 'price' => 67.99, 359 | 'quantity' => 3, 360 | 'attributes' => array() 361 | ), 362 | array( 363 | 'id' => 568, 364 | 'name' => 'Sample Item 2', 365 | 'price' => 69.25, 366 | 'quantity' => 1, 367 | 'attributes' => array() 368 | ), 369 | ); 370 | 371 | $this->cart->add($items); 372 | 373 | $this->assertEquals(273.22, $this->cart->getSubTotal(), 'Cart should have sub total of 273.22'); 374 | 375 | // when cart's item quantity is updated, the subtotal should be updated as well 376 | $this->cart->update(456, array('quantity' => -1)); 377 | 378 | // get the item to be evaluated 379 | $item = $this->cart->get(456); 380 | 381 | $this->assertEquals(2, $item['quantity'], 'Item quantity of with item ID of 456 should now be reduced to 2'); 382 | $this->assertEquals(205.23, $this->cart->getSubTotal(), 'Cart should have sub total of 205.23'); 383 | } 384 | 385 | public function test_item_quantity_update_by_reduced_should_not_reduce_if_quantity_will_result_to_zero() 386 | { 387 | $items = array( 388 | array( 389 | 'id' => 456, 390 | 'name' => 'Sample Item 1', 391 | 'price' => 67.99, 392 | 'quantity' => 3, 393 | 'attributes' => array() 394 | ), 395 | array( 396 | 'id' => 568, 397 | 'name' => 'Sample Item 2', 398 | 'price' => 69.25, 399 | 'quantity' => 1, 400 | 'attributes' => array() 401 | ), 402 | ); 403 | 404 | $this->cart->add($items); 405 | 406 | // get the item to be evaluated 407 | $item = $this->cart->get(456); 408 | 409 | // prove first we have quantity of 3 410 | $this->assertEquals(3, $item['quantity'], 'Item quantity of with item ID of 456 should be reduced to 3'); 411 | 412 | // when cart's item quantity is updated, and reduced to more than the current quantity 413 | // this should not work 414 | $this->cart->update(456, array('quantity' => -3)); 415 | 416 | $this->assertEquals(3, $item['quantity'], 'Item quantity of with item ID of 456 should now be reduced to 2'); 417 | } 418 | 419 | public function test_should_throw_exception_when_provided_invalid_values_scenario_one() 420 | { 421 | $this->expectException('Darryldecode\Cart\Exceptions\InvalidItemException'); 422 | $this->cart->add(455, 'Sample Item', 100.99, 0, array()); 423 | } 424 | 425 | public function test_should_throw_exception_when_provided_invalid_values_scenario_two() 426 | { 427 | $this->expectException('Darryldecode\Cart\Exceptions\InvalidItemException'); 428 | $this->cart->add('', 'Sample Item', 100.99, 2, array()); 429 | } 430 | 431 | public function test_should_throw_exception_when_provided_invalid_values_scenario_three() 432 | { 433 | $this->expectException('Darryldecode\Cart\Exceptions\InvalidItemException'); 434 | $this->cart->add(523, '', 100.99, 2, array()); 435 | } 436 | 437 | public function test_clearing_cart() 438 | { 439 | $items = array( 440 | array( 441 | 'id' => 456, 442 | 'name' => 'Sample Item 1', 443 | 'price' => 67.99, 444 | 'quantity' => 3, 445 | 'attributes' => array() 446 | ), 447 | array( 448 | 'id' => 568, 449 | 'name' => 'Sample Item 2', 450 | 'price' => 69.25, 451 | 'quantity' => 1, 452 | 'attributes' => array() 453 | ), 454 | ); 455 | 456 | $this->cart->add($items); 457 | 458 | $this->assertFalse($this->cart->isEmpty(), 'prove first cart is not empty'); 459 | 460 | // now let's clear cart 461 | $this->cart->clear(); 462 | 463 | $this->assertTrue($this->cart->isEmpty(), 'cart should now be empty'); 464 | } 465 | 466 | public function test_cart_get_total_quantity() 467 | { 468 | $items = array( 469 | array( 470 | 'id' => 456, 471 | 'name' => 'Sample Item 1', 472 | 'price' => 67.99, 473 | 'quantity' => 3, 474 | 'attributes' => array() 475 | ), 476 | array( 477 | 'id' => 568, 478 | 'name' => 'Sample Item 2', 479 | 'price' => 69.25, 480 | 'quantity' => 1, 481 | 'attributes' => array() 482 | ), 483 | ); 484 | 485 | $this->cart->add($items); 486 | 487 | $this->assertFalse($this->cart->isEmpty(), 'prove first cart is not empty'); 488 | 489 | // now let's count the cart's quantity 490 | $this->assertIsInt($this->cart->getTotalQuantity(), 'Return type should be INT'); 491 | $this->assertEquals(4, $this->cart->getTotalQuantity(), 'Cart\'s quantity should be 4.'); 492 | } 493 | 494 | public function test_cart_can_add_items_as_array_with_associated_model() 495 | { 496 | $item = array( 497 | 'id' => 456, 498 | 'name' => 'Sample Item', 499 | 'price' => 67.99, 500 | 'quantity' => 4, 501 | 'attributes' => array(), 502 | 'associatedModel' => MockProduct::class 503 | ); 504 | 505 | $this->cart->add($item); 506 | 507 | $addedItem = $this->cart->get($item['id']); 508 | 509 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 510 | $this->assertEquals(1, $this->cart->getContent()->count(), 'Cart should have 1 item on it'); 511 | $this->assertEquals(456, $this->cart->getContent()->first()['id'], 'The first content must have ID of 456'); 512 | $this->assertEquals('Sample Item', $this->cart->getContent()->first()['name'], 'The first content must have name of "Sample Item"'); 513 | $this->assertInstanceOf('Darryldecode\Tests\helpers\MockProduct', $addedItem->model); 514 | } 515 | 516 | public function test_cart_can_add_items_with_multidimensional_array_with_associated_model() 517 | { 518 | $items = array( 519 | array( 520 | 'id' => 456, 521 | 'name' => 'Sample Item 1', 522 | 'price' => 67.99, 523 | 'quantity' => 4, 524 | 'attributes' => array(), 525 | 'associatedModel' => MockProduct::class 526 | ), 527 | array( 528 | 'id' => 568, 529 | 'name' => 'Sample Item 2', 530 | 'price' => 69.25, 531 | 'quantity' => 4, 532 | 'attributes' => array(), 533 | 'associatedModel' => MockProduct::class 534 | ), 535 | array( 536 | 'id' => 856, 537 | 'name' => 'Sample Item 3', 538 | 'price' => 50.25, 539 | 'quantity' => 4, 540 | 'attributes' => array(), 541 | 'associatedModel' => MockProduct::class 542 | ), 543 | ); 544 | 545 | $this->cart->add($items); 546 | 547 | $content = $this->cart->getContent(); 548 | foreach ($content as $item) { 549 | $this->assertInstanceOf('Darryldecode\Tests\helpers\MockProduct', $item->model); 550 | } 551 | 552 | $this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty'); 553 | $this->assertCount(3, $this->cart->getContent()->toArray(), 'Cart should have 3 items'); 554 | $this->assertIsInt($this->cart->getTotalQuantity(), 'Return type should be INT'); 555 | $this->assertEquals(12, $this->cart->getTotalQuantity(), 'Cart\'s quantity should be 4.'); 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /src/Darryldecode/Cart/Cart.php: -------------------------------------------------------------------------------- 1 | events = $events; 84 | $this->session = $session; 85 | $this->instanceName = $instanceName; 86 | $this->sessionKey = $session_key; 87 | $this->sessionKeyCartItems = $this->sessionKey . '_cart_items'; 88 | $this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions'; 89 | $this->config = $config; 90 | $this->currentItemId = null; 91 | $this->fireEvent('created'); 92 | } 93 | 94 | /** 95 | * sets the session key 96 | * 97 | * @param string $sessionKey the session key or identifier 98 | * @return $this|bool 99 | * @throws \Exception 100 | */ 101 | public function session($sessionKey) 102 | { 103 | if (!$sessionKey) throw new \Exception("Session key is required."); 104 | 105 | $this->sessionKey = $sessionKey; 106 | $this->sessionKeyCartItems = $this->sessionKey . '_cart_items'; 107 | $this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions'; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * get instance name of the cart 114 | * 115 | * @return string 116 | */ 117 | public function getInstanceName() 118 | { 119 | return $this->instanceName; 120 | } 121 | 122 | /** 123 | * get an item on a cart by item ID 124 | * 125 | * @param $itemId 126 | * @return mixed 127 | */ 128 | public function get($itemId) 129 | { 130 | return $this->getContent()->get($itemId); 131 | } 132 | 133 | /** 134 | * check if an item exists by item ID 135 | * 136 | * @param $itemId 137 | * @return bool 138 | */ 139 | public function has($itemId) 140 | { 141 | return $this->getContent()->has($itemId); 142 | } 143 | 144 | /** 145 | * add item to the cart, it can be an array or multi dimensional array 146 | * 147 | * @param string|array $id 148 | * @param string $name 149 | * @param float $price 150 | * @param int $quantity 151 | * @param array $attributes 152 | * @param CartCondition|array $conditions 153 | * @param string $associatedModel 154 | * @return $this 155 | * @throws InvalidItemException 156 | */ 157 | public function add($id, $name = null, $price = null, $quantity = null, $attributes = array(), $conditions = array(), $associatedModel = null) 158 | { 159 | // if the first argument is an array, 160 | // we will need to call add again 161 | if (is_array($id)) { 162 | // the first argument is an array, now we will need to check if it is a multi dimensional 163 | // array, if so, we will iterate through each item and call add again 164 | if (Helpers::isMultiArray($id)) { 165 | foreach ($id as $item) { 166 | $this->add( 167 | $item['id'], 168 | $item['name'], 169 | $item['price'], 170 | $item['quantity'], 171 | Helpers::issetAndHasValueOrAssignDefault($item['attributes'], array()), 172 | Helpers::issetAndHasValueOrAssignDefault($item['conditions'], array()), 173 | Helpers::issetAndHasValueOrAssignDefault($item['associatedModel'], null) 174 | ); 175 | } 176 | } else { 177 | $this->add( 178 | $id['id'], 179 | $id['name'], 180 | $id['price'], 181 | $id['quantity'], 182 | Helpers::issetAndHasValueOrAssignDefault($id['attributes'], array()), 183 | Helpers::issetAndHasValueOrAssignDefault($id['conditions'], array()), 184 | Helpers::issetAndHasValueOrAssignDefault($id['associatedModel'], null) 185 | ); 186 | } 187 | 188 | return $this; 189 | } 190 | 191 | $data = array( 192 | 'id' => $id, 193 | 'name' => $name, 194 | 'price' => Helpers::normalizePrice($price), 195 | 'quantity' => $quantity, 196 | 'attributes' => new ItemAttributeCollection($attributes), 197 | 'conditions' => $conditions 198 | ); 199 | 200 | if (isset($associatedModel) && $associatedModel != '') { 201 | $data['associatedModel'] = $associatedModel; 202 | } 203 | 204 | // validate data 205 | $item = $this->validate($data); 206 | 207 | // get the cart 208 | $cart = $this->getContent(); 209 | 210 | // if the item is already in the cart we will just update it 211 | if ($cart->has($id)) { 212 | 213 | $this->update($id, $item); 214 | } else { 215 | 216 | $this->addRow($id, $item); 217 | } 218 | 219 | $this->currentItemId = $id; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * update a cart 226 | * 227 | * @param $id 228 | * @param array $data 229 | * 230 | * the $data will be an associative array, you don't need to pass all the data, only the key value 231 | * of the item you want to update on it 232 | * @return bool 233 | */ 234 | public function update($id, $data) 235 | { 236 | if ($this->fireEvent('updating', $data) === false) { 237 | return false; 238 | } 239 | 240 | $cart = $this->getContent(); 241 | 242 | $item = $cart->pull($id); 243 | 244 | foreach ($data as $key => $value) { 245 | // if the key is currently "quantity" we will need to check if an arithmetic 246 | // symbol is present so we can decide if the update of quantity is being added 247 | // or being reduced. 248 | if ($key == 'quantity') { 249 | // we will check if quantity value provided is array, 250 | // if it is, we will need to check if a key "relative" is set 251 | // and we will evaluate its value if true or false, 252 | // this tells us how to treat the quantity value if it should be updated 253 | // relatively to its current quantity value or just totally replace the value 254 | if (is_array($value)) { 255 | if (isset($value['relative'])) { 256 | if ((bool)$value['relative']) { 257 | $item = $this->updateQuantityRelative($item, $key, $value['value']); 258 | } else { 259 | $item = $this->updateQuantityNotRelative($item, $key, $value['value']); 260 | } 261 | } 262 | } else { 263 | $item = $this->updateQuantityRelative($item, $key, $value); 264 | } 265 | } elseif ($key == 'attributes') { 266 | $item[$key] = new ItemAttributeCollection($value); 267 | } else { 268 | $item[$key] = $value; 269 | } 270 | } 271 | 272 | $cart->put($id, $item); 273 | 274 | $this->save($cart); 275 | 276 | $this->fireEvent('updated', $item); 277 | return true; 278 | } 279 | 280 | /** 281 | * add condition on an existing item on the cart 282 | * 283 | * @param int|string $productId 284 | * @param CartCondition $itemCondition 285 | * @return $this 286 | */ 287 | public function addItemCondition($productId, $itemCondition) 288 | { 289 | if ($product = $this->get($productId)) { 290 | $conditionInstance = "\\Darryldecode\\Cart\\CartCondition"; 291 | 292 | if ($itemCondition instanceof $conditionInstance) { 293 | // we need to copy first to a temporary variable to hold the conditions 294 | // to avoid hitting this error "Indirect modification of overloaded element of Darryldecode\Cart\ItemCollection has no effect" 295 | // this is due to laravel Collection instance that implements Array Access 296 | // // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect 297 | $itemConditionTempHolder = $product['conditions']; 298 | 299 | if (is_array($itemConditionTempHolder)) { 300 | array_push($itemConditionTempHolder, $itemCondition); 301 | } else { 302 | $itemConditionTempHolder = $itemCondition; 303 | } 304 | 305 | $this->update($productId, array( 306 | 'conditions' => $itemConditionTempHolder // the newly updated conditions 307 | )); 308 | } 309 | } 310 | 311 | return $this; 312 | } 313 | 314 | /** 315 | * removes an item on cart by item ID 316 | * 317 | * @param $id 318 | * @return bool 319 | */ 320 | public function remove($id) 321 | { 322 | $cart = $this->getContent(); 323 | 324 | if ($this->fireEvent('removing', $id) === false) { 325 | return false; 326 | } 327 | 328 | $cart->forget($id); 329 | 330 | $this->save($cart); 331 | 332 | $this->fireEvent('removed', $id); 333 | return true; 334 | } 335 | 336 | /** 337 | * clear cart 338 | * @return bool 339 | */ 340 | public function clear() 341 | { 342 | if ($this->fireEvent('clearing') === false) { 343 | return false; 344 | } 345 | 346 | $this->session->put( 347 | $this->sessionKeyCartItems, 348 | array() 349 | ); 350 | 351 | $this->fireEvent('cleared'); 352 | return true; 353 | } 354 | 355 | /** 356 | * add a condition on the cart 357 | * 358 | * @param CartCondition|array $condition 359 | * @return $this 360 | * @throws InvalidConditionException 361 | */ 362 | public function condition($condition) 363 | { 364 | if (is_array($condition)) { 365 | foreach ($condition as $c) { 366 | $this->condition($c); 367 | } 368 | 369 | return $this; 370 | } 371 | 372 | if (!$condition instanceof CartCondition) throw new InvalidConditionException('Argument 1 must be an instance of \'Darryldecode\Cart\CartCondition\''); 373 | 374 | $conditions = $this->getConditions(); 375 | 376 | // Check if order has been applied 377 | if ($condition->getOrder() == 0) { 378 | $last = $conditions->last(); 379 | $condition->setOrder(!is_null($last) ? $last->getOrder() + 1 : 1); 380 | } 381 | 382 | $conditions->put($condition->getName(), $condition); 383 | 384 | $conditions = $conditions->sortBy(function ($condition, $key) { 385 | return $condition->getOrder(); 386 | }); 387 | 388 | $this->saveConditions($conditions); 389 | 390 | return $this; 391 | } 392 | 393 | /** 394 | * get conditions applied on the cart 395 | * 396 | * @return CartConditionCollection 397 | */ 398 | public function getConditions() 399 | { 400 | return new CartConditionCollection($this->session->get($this->sessionKeyCartConditions)); 401 | } 402 | 403 | /** 404 | * get condition applied on the cart by its name 405 | * 406 | * @param $conditionName 407 | * @return CartCondition 408 | */ 409 | public function getCondition($conditionName) 410 | { 411 | return $this->getConditions()->get($conditionName); 412 | } 413 | 414 | /** 415 | * Get all the condition filtered by Type 416 | * Please Note that this will only return condition added on cart bases, not those conditions added 417 | * specifically on an per item bases 418 | * 419 | * @param $type 420 | * @return CartConditionCollection 421 | */ 422 | public function getConditionsByType($type) 423 | { 424 | return $this->getConditions()->filter(function (CartCondition $condition) use ($type) { 425 | return $condition->getType() == $type; 426 | }); 427 | } 428 | 429 | 430 | /** 431 | * Remove all the condition with the $type specified 432 | * Please Note that this will only remove condition added on cart bases, not those conditions added 433 | * specifically on an per item bases 434 | * 435 | * @param $type 436 | * @return $this 437 | */ 438 | public function removeConditionsByType($type) 439 | { 440 | $this->getConditionsByType($type)->each(function ($condition) { 441 | $this->removeCartCondition($condition->getName()); 442 | }); 443 | } 444 | 445 | 446 | /** 447 | * removes a condition on a cart by condition name, 448 | * this can only remove conditions that are added on cart bases not conditions that are added on an item/product. 449 | * If you wish to remove a condition that has been added for a specific item/product, you may 450 | * use the removeItemCondition(itemId, conditionName) method instead. 451 | * 452 | * @param $conditionName 453 | * @return void 454 | */ 455 | public function removeCartCondition($conditionName) 456 | { 457 | $conditions = $this->getConditions(); 458 | 459 | $conditions->pull($conditionName); 460 | 461 | $this->saveConditions($conditions); 462 | } 463 | 464 | /** 465 | * remove a condition that has been applied on an item that is already on the cart 466 | * 467 | * @param $itemId 468 | * @param $conditionName 469 | * @return bool 470 | */ 471 | public function removeItemCondition($itemId, $conditionName) 472 | { 473 | if (!$item = $this->getContent()->get($itemId)) { 474 | return false; 475 | } 476 | 477 | if ($this->itemHasConditions($item)) { 478 | // NOTE: 479 | // we do it this way, we get first conditions and store 480 | // it in a temp variable $originalConditions, then we will modify the array there 481 | // and after modification we will store it again on $item['conditions'] 482 | // This is because of ArrayAccess implementation 483 | // see link for more info: http://stackoverflow.com/questions/20053269/indirect-modification-of-overloaded-element-of-splfixedarray-has-no-effect 484 | 485 | $tempConditionsHolder = $item['conditions']; 486 | 487 | // if the item's conditions is in array format 488 | // we will iterate through all of it and check if the name matches 489 | // to the given name the user wants to remove, if so, remove it 490 | if (is_array($tempConditionsHolder)) { 491 | foreach ($tempConditionsHolder as $k => $condition) { 492 | if ($condition->getName() == $conditionName) { 493 | unset($tempConditionsHolder[$k]); 494 | } 495 | } 496 | 497 | $item['conditions'] = $tempConditionsHolder; 498 | } 499 | 500 | // if the item condition is not an array, we will check if it is 501 | // an instance of a Condition, if so, we will check if the name matches 502 | // on the given condition name the user wants to remove, if so, 503 | // lets just make $item['conditions'] an empty array as there's just 1 condition on it anyway 504 | else { 505 | $conditionInstance = "Darryldecode\\Cart\\CartCondition"; 506 | 507 | if ($item['conditions'] instanceof $conditionInstance) { 508 | if ($tempConditionsHolder->getName() == $conditionName) { 509 | $item['conditions'] = array(); 510 | } 511 | } 512 | } 513 | } 514 | 515 | $this->update($itemId, array( 516 | 'conditions' => $item['conditions'] 517 | )); 518 | 519 | return true; 520 | } 521 | 522 | /** 523 | * remove all conditions that has been applied on an item that is already on the cart 524 | * 525 | * @param $itemId 526 | * @return bool 527 | */ 528 | public function clearItemConditions($itemId) 529 | { 530 | if (!$item = $this->getContent()->get($itemId)) { 531 | return false; 532 | } 533 | 534 | $this->update($itemId, array( 535 | 'conditions' => array() 536 | )); 537 | 538 | return true; 539 | } 540 | 541 | /** 542 | * clears all conditions on a cart, 543 | * this does not remove conditions that has been added specifically to an item/product. 544 | * If you wish to remove a specific condition to a product, you may use the method: removeItemCondition($itemId, $conditionName) 545 | * 546 | * @return void 547 | */ 548 | public function clearCartConditions() 549 | { 550 | $this->session->put( 551 | $this->sessionKeyCartConditions, 552 | array() 553 | ); 554 | } 555 | 556 | /** 557 | * get cart sub total without conditions 558 | * @param bool $formatted 559 | * @return float 560 | */ 561 | public function getSubTotalWithoutConditions($formatted = true) 562 | { 563 | $cart = $this->getContent(); 564 | 565 | $sum = $cart->sum(function ($item) { 566 | return $item->getPriceSum(); 567 | }); 568 | 569 | return Helpers::formatValue(floatval($sum), $formatted, $this->config); 570 | } 571 | 572 | /** 573 | * get cart sub total 574 | * @param bool $formatted 575 | * @return float 576 | */ 577 | public function getSubTotal($formatted = true) 578 | { 579 | $cart = $this->getContent(); 580 | 581 | $sum = $cart->sum(function (ItemCollection $item) { 582 | return $item->getPriceSumWithConditions(false); 583 | }); 584 | 585 | // get the conditions that are meant to be applied 586 | // on the subtotal and apply it here before returning the subtotal 587 | $conditions = $this 588 | ->getConditions() 589 | ->filter(function (CartCondition $cond) { 590 | return $cond->getTarget() === 'subtotal'; 591 | }); 592 | 593 | // if there is no conditions, lets just return the sum 594 | if (!$conditions->count()) return Helpers::formatValue(floatval($sum), $formatted, $this->config); 595 | 596 | // there are conditions, lets apply it 597 | $newTotal = 0.00; 598 | $process = 0; 599 | 600 | $conditions->each(function (CartCondition $cond) use ($sum, &$newTotal, &$process) { 601 | 602 | // if this is the first iteration, the toBeCalculated 603 | // should be the sum as initial point of value. 604 | $toBeCalculated = ($process > 0) ? $newTotal : $sum; 605 | 606 | $newTotal = $cond->applyCondition($toBeCalculated); 607 | 608 | $process++; 609 | }); 610 | 611 | return Helpers::formatValue(floatval($newTotal), $formatted, $this->config); 612 | } 613 | 614 | /** 615 | * the new total in which conditions are already applied 616 | * 617 | * @return float 618 | */ 619 | public function getTotal() 620 | { 621 | $subTotal = $this->getSubTotal(false); 622 | 623 | $newTotal = 0.00; 624 | 625 | $process = 0; 626 | 627 | $conditions = $this 628 | ->getConditions() 629 | ->filter(function (CartCondition $cond) { 630 | return $cond->getTarget() === 'total'; 631 | }); 632 | 633 | // if no conditions were added, just return the sub total 634 | if (!$conditions->count()) { 635 | return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config); 636 | } 637 | 638 | $conditions 639 | ->each(function (CartCondition $cond) use ($subTotal, &$newTotal, &$process) { 640 | $toBeCalculated = ($process > 0) ? $newTotal : $subTotal; 641 | 642 | $newTotal = $cond->applyCondition($toBeCalculated); 643 | 644 | $process++; 645 | }); 646 | 647 | return Helpers::formatValue($newTotal, $this->config['format_numbers'], $this->config); 648 | } 649 | 650 | /** 651 | * get total quantity of items in the cart 652 | * 653 | * @return int 654 | */ 655 | public function getTotalQuantity() 656 | { 657 | $items = $this->getContent(); 658 | 659 | if ($items->isEmpty()) return 0; 660 | 661 | $count = $items->sum(function ($item) { 662 | return $item['quantity']; 663 | }); 664 | 665 | return $count; 666 | } 667 | 668 | /** 669 | * get the cart 670 | * 671 | * @return CartCollection 672 | */ 673 | public function getContent() 674 | { 675 | return (new CartCollection($this->session->get($this->sessionKeyCartItems)))->reject(function($item) { 676 | return ! ($item instanceof ItemCollection); 677 | }); 678 | } 679 | 680 | /** 681 | * check if cart is empty 682 | * 683 | * @return bool 684 | */ 685 | public function isEmpty() 686 | { 687 | return $this->getContent()->isEmpty(); 688 | } 689 | 690 | /** 691 | * validate Item data 692 | * 693 | * @param $item 694 | * @return array $item; 695 | * @throws InvalidItemException 696 | */ 697 | protected function validate($item) 698 | { 699 | $rules = array( 700 | 'id' => 'required', 701 | 'price' => 'required|numeric', 702 | 'quantity' => 'required|numeric|min:0.1', 703 | 'name' => 'required', 704 | ); 705 | 706 | $validator = CartItemValidator::make($item, $rules); 707 | 708 | if ($validator->fails()) { 709 | throw new InvalidItemException($validator->messages()->first()); 710 | } 711 | 712 | return $item; 713 | } 714 | 715 | /** 716 | * add row to cart collection 717 | * 718 | * @param $id 719 | * @param $item 720 | * @return bool 721 | */ 722 | protected function addRow($id, $item) 723 | { 724 | if ($this->fireEvent('adding', $item) === false) { 725 | return false; 726 | } 727 | 728 | $cart = $this->getContent(); 729 | 730 | $cart->put($id, new ItemCollection($item, $this->config)); 731 | 732 | $this->save($cart); 733 | 734 | $this->fireEvent('added', $item); 735 | 736 | return true; 737 | } 738 | 739 | /** 740 | * save the cart 741 | * 742 | * @param $cart CartCollection 743 | */ 744 | protected function save($cart) 745 | { 746 | $this->session->put($this->sessionKeyCartItems, $cart); 747 | } 748 | 749 | /** 750 | * save the cart conditions 751 | * 752 | * @param $conditions 753 | */ 754 | protected function saveConditions($conditions) 755 | { 756 | $this->session->put($this->sessionKeyCartConditions, $conditions); 757 | } 758 | 759 | /** 760 | * check if an item has condition 761 | * 762 | * @param $item 763 | * @return bool 764 | */ 765 | protected function itemHasConditions($item) 766 | { 767 | if (!isset($item['conditions'])) return false; 768 | 769 | if (is_array($item['conditions'])) { 770 | return count($item['conditions']) > 0; 771 | } 772 | 773 | $conditionInstance = "Darryldecode\\Cart\\CartCondition"; 774 | 775 | if ($item['conditions'] instanceof $conditionInstance) return true; 776 | 777 | return false; 778 | } 779 | 780 | /** 781 | * update a cart item quantity relative to its current quantity 782 | * 783 | * @param $item 784 | * @param $key 785 | * @param $value 786 | * @return mixed 787 | */ 788 | protected function updateQuantityRelative($item, $key, $value) 789 | { 790 | if (preg_match('/\-/', $value) == 1) { 791 | $value = (int)str_replace('-', '', $value); 792 | 793 | // we will not allowed to reduced quantity to 0, so if the given value 794 | // would result to item quantity of 0, we will not do it. 795 | if (($item[$key] - $value) > 0) { 796 | $item[$key] -= $value; 797 | } 798 | } elseif (preg_match('/\+/', $value) == 1) { 799 | $item[$key] += (int)str_replace('+', '', $value); 800 | } else { 801 | $item[$key] += (int)$value; 802 | } 803 | 804 | return $item; 805 | } 806 | 807 | /** 808 | * update cart item quantity not relative to its current quantity value 809 | * 810 | * @param $item 811 | * @param $key 812 | * @param $value 813 | * @return mixed 814 | */ 815 | protected function updateQuantityNotRelative($item, $key, $value) 816 | { 817 | $item[$key] = (int)$value; 818 | 819 | return $item; 820 | } 821 | 822 | /** 823 | * Setter for decimals. Change value on demand. 824 | * @param $decimals 825 | */ 826 | public function setDecimals($decimals) 827 | { 828 | $this->decimals = $decimals; 829 | } 830 | 831 | /** 832 | * Setter for decimals point. Change value on demand. 833 | * @param $dec_point 834 | */ 835 | public function setDecPoint($dec_point) 836 | { 837 | $this->dec_point = $dec_point; 838 | } 839 | 840 | public function setThousandsSep($thousands_sep) 841 | { 842 | $this->thousands_sep = $thousands_sep; 843 | } 844 | 845 | /** 846 | * @param $name 847 | * @param $value 848 | * @return mixed 849 | */ 850 | protected function fireEvent($name, $value = []) 851 | { 852 | return $this->events->dispatch($this->getInstanceName() . '.' . $name, array_values([$value, $this]), true); 853 | } 854 | 855 | /** 856 | * Associate the cart item with the given id with the given model. 857 | * 858 | * @param string $id 859 | * @param mixed $model 860 | * 861 | * @return void 862 | */ 863 | public function associate($model) 864 | { 865 | if (is_string($model) && !class_exists($model)) { 866 | throw new UnknownModelException("The supplied model {$model} does not exist."); 867 | } 868 | 869 | $cart = $this->getContent(); 870 | 871 | $item = $cart->pull($this->currentItemId); 872 | 873 | $item['associatedModel'] = $model; 874 | 875 | $cart->put($this->currentItemId, new ItemCollection($item, $this->config)); 876 | 877 | $this->save($cart); 878 | 879 | return $this; 880 | } 881 | } 882 | -------------------------------------------------------------------------------- /tests/CartConditionsTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch'); 26 | 27 | $this->cart = new Cart( 28 | new SessionMock(), 29 | $events, 30 | 'shopping', 31 | 'SAMPLESESSIONKEY', 32 | require(__DIR__.'/helpers/configMock.php') 33 | ); 34 | } 35 | 36 | public function tearDown(): void 37 | { 38 | m::close(); 39 | } 40 | 41 | public function test_subtotal() 42 | { 43 | $this->fillCart(); 44 | 45 | // add condition to subtotal 46 | $condition = new CartCondition(array( 47 | 'name' => 'VAT 12.5%', 48 | 'type' => 'tax', 49 | 'target' => 'subtotal', 50 | 'value' => '-5', 51 | )); 52 | 53 | $this->cart->condition($condition); 54 | 55 | $this->assertEquals(182.49,$this->cart->getSubTotal()); 56 | 57 | // the total is also should be the same with sub total since our getTotal 58 | // also depends on what is the value of subtotal 59 | $this->assertEquals(182.49,$this->cart->getTotal()); 60 | } 61 | 62 | public function test_total_without_condition() 63 | { 64 | $this->fillCart(); 65 | 66 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 67 | 68 | // no changes in subtotal as the condition's target added was for total 69 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 70 | 71 | // total should be the same as subtotal 72 | $this->assertEquals(187.49, $this->cart->getTotal(), 'Cart should have a total of 187.49'); 73 | } 74 | 75 | public function test_total_with_condition() 76 | { 77 | $this->fillCart(); 78 | 79 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 80 | 81 | // add condition 82 | $condition = new CartCondition(array( 83 | 'name' => 'VAT 12.5%', 84 | 'type' => 'tax', 85 | 'target' => 'total', 86 | 'value' => '12.5%', 87 | )); 88 | 89 | $this->cart->condition($condition); 90 | 91 | // no changes in subtotal as the condition's target added was for total 92 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 93 | 94 | // total should be changed 95 | $this->cart->setDecimals(5); 96 | $this->assertEquals(210.92625, $this->cart->getTotal(), 'Cart should have a total of 210.92625'); 97 | } 98 | 99 | public function test_total_with_multiple_conditions_added_scenario_one() 100 | { 101 | $this->fillCart(); 102 | 103 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 104 | 105 | // add condition 106 | $condition1 = new CartCondition(array( 107 | 'name' => 'VAT 12.5%', 108 | 'type' => 'tax', 109 | 'target' => 'total', 110 | 'value' => '12.5%', 111 | )); 112 | $condition2 = new CartCondition(array( 113 | 'name' => 'Express Shipping $15', 114 | 'type' => 'shipping', 115 | 'target' => 'total', 116 | 'value' => '+15', 117 | )); 118 | 119 | $this->cart->condition($condition1); 120 | $this->cart->condition($condition2); 121 | 122 | // no changes in subtotal as the condition's target added was for subtotal 123 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 124 | 125 | // total should be changed 126 | $this->cart->setDecimals(5); 127 | $this->assertEquals(225.92625, $this->cart->getTotal(), 'Cart should have a total of 225.92625'); 128 | } 129 | 130 | public function test_total_with_multiple_conditions_added_scenario_two() 131 | { 132 | $this->fillCart(); 133 | 134 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 135 | 136 | // add condition 137 | $condition1 = new CartCondition(array( 138 | 'name' => 'VAT 12.5%', 139 | 'type' => 'tax', 140 | 'target' => 'total', 141 | 'value' => '12.5%', 142 | )); 143 | $condition2 = new CartCondition(array( 144 | 'name' => 'Express Shipping $15', 145 | 'type' => 'shipping', 146 | 'target' => 'total', 147 | 'value' => '-15', 148 | )); 149 | 150 | $this->cart->condition($condition1); 151 | $this->cart->condition($condition2); 152 | 153 | // no changes in subtotal as the condition's target added was for subtotal 154 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 155 | 156 | // total should be changed 157 | $this->cart->setDecimals(5); 158 | $this->assertEquals(195.92625, $this->cart->getTotal(), 'Cart should have a total of 195.92625'); 159 | } 160 | 161 | public function test_total_with_multiple_conditions_added_scenario_three() 162 | { 163 | $this->fillCart(); 164 | 165 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 166 | 167 | // add condition 168 | $condition1 = new CartCondition(array( 169 | 'name' => 'VAT 12.5%', 170 | 'type' => 'tax', 171 | 'target' => 'total', 172 | 'value' => '-12.5%', 173 | )); 174 | $condition2 = new CartCondition(array( 175 | 'name' => 'Express Shipping $15', 176 | 'type' => 'shipping', 177 | 'target' => 'total', 178 | 'value' => '-15', 179 | )); 180 | 181 | $this->cart->condition($condition1); 182 | $this->cart->condition($condition2); 183 | 184 | // no changes in subtotal as the condition's target added was for total 185 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 186 | 187 | // total should be changed 188 | $this->cart->setDecimals(5); 189 | $this->assertEquals(149.05375, $this->cart->getTotal(), 'Cart should have a total of 149.05375'); 190 | } 191 | 192 | public function test_cart_multiple_conditions_can_be_added_once_by_array() 193 | { 194 | $this->fillCart(); 195 | 196 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 197 | 198 | // add condition 199 | $condition1 = new CartCondition(array( 200 | 'name' => 'VAT 12.5%', 201 | 'type' => 'tax', 202 | 'target' => 'total', 203 | 'value' => '-12.5%', 204 | )); 205 | $condition2 = new CartCondition(array( 206 | 'name' => 'Express Shipping $15', 207 | 'type' => 'shipping', 208 | 'target' => 'total', 209 | 'value' => '-15', 210 | )); 211 | 212 | $this->cart->condition([$condition1,$condition2]); 213 | 214 | // no changes in subtotal as the condition's target added was for total 215 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 216 | 217 | // total should be changed 218 | $this->cart->setDecimals(5); 219 | $this->assertEquals(149.05375, $this->cart->getTotal(), 'Cart should have a total of 149.05375'); 220 | } 221 | 222 | public function test_total_with_multiple_conditions_added_scenario_four() 223 | { 224 | $this->fillCart(); 225 | 226 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 227 | 228 | // add condition 229 | $condition1 = new CartCondition(array( 230 | 'name' => 'COUPON LESS 12.5%', 231 | 'type' => 'tax', 232 | 'target' => 'total', 233 | 'value' => '-12.5%', 234 | )); 235 | $condition2 = new CartCondition(array( 236 | 'name' => 'Express Shipping $15', 237 | 'type' => 'shipping', 238 | 'target' => 'total', 239 | 'value' => '+15', 240 | )); 241 | 242 | $this->cart->condition($condition1); 243 | $this->cart->condition($condition2); 244 | 245 | // no changes in subtotal as the condition's target added was for total 246 | $this->assertEquals(187.49, $this->cart->getSubTotal(), 'Cart should have sub total of 187.49'); 247 | 248 | // total should be changed 249 | $this->cart->setDecimals(5); 250 | $this->assertEquals(179.05375, $this->cart->getTotal(), 'Cart should have a total of 179.05375'); 251 | } 252 | 253 | public function test_add_item_with_condition() 254 | { 255 | $condition1 = new CartCondition(array( 256 | 'name' => 'SALE 5%', 257 | 'type' => 'tax', 258 | 'value' => '-5%', 259 | )); 260 | 261 | $item = array( 262 | 'id' => 456, 263 | 'name' => 'Sample Item 1', 264 | 'price' => 100, 265 | 'quantity' => 1, 266 | 'attributes' => array(), 267 | 'conditions' => $condition1 268 | ); 269 | 270 | $this->cart->add($item); 271 | 272 | $this->assertEquals(95, $this->cart->get(456)->getPriceSumWithConditions()); 273 | $this->assertEquals(95, $this->cart->getSubTotal()); 274 | } 275 | 276 | public function test_add_item_with_multiple_item_conditions_in_multiple_condition_instance() 277 | { 278 | $itemCondition1 = new CartCondition(array( 279 | 'name' => 'SALE 5%', 280 | 'type' => 'sale', 281 | 'value' => '-5%', 282 | )); 283 | $itemCondition2 = new CartCondition(array( 284 | 'name' => 'Item Gift Pack 25.00', 285 | 'type' => 'promo', 286 | 'value' => '-25', 287 | )); 288 | $itemCondition3 = new CartCondition(array( 289 | 'name' => 'MISC', 290 | 'type' => 'misc', 291 | 'value' => '+10', 292 | )); 293 | 294 | $item = array( 295 | 'id' => 456, 296 | 'name' => 'Sample Item 1', 297 | 'price' => 100, 298 | 'quantity' => 1, 299 | 'attributes' => array(), 300 | 'conditions' => [$itemCondition1, $itemCondition2, $itemCondition3] 301 | ); 302 | 303 | $this->cart->add($item); 304 | 305 | $this->assertEquals(80.00, $this->cart->get(456)->getPriceSumWithConditions(), 'Item subtotal with 1 item should be 80'); 306 | $this->assertEquals(80.00, $this->cart->getSubTotal(), 'Cart subtotal with 1 item should be 80'); 307 | } 308 | 309 | public function test_add_item_with_multiple_item_conditions_with_target_omitted() 310 | { 311 | // NOTE: 312 | // $condition1 and $condition4 should not be included in calculation 313 | // as the target is not for item, remember that when adding 314 | // conditions in per-item bases, the condition's target should 315 | // have a value of item 316 | 317 | $itemCondition2 = new CartCondition(array( 318 | 'name' => 'Item Gift Pack 25.00', 319 | 'type' => 'promo', 320 | 'value' => '-25', 321 | )); 322 | $itemCondition3 = new CartCondition(array( 323 | 'name' => 'MISC', 324 | 'type' => 'misc', 325 | 'value' => '+10', 326 | )); 327 | 328 | $item = array( 329 | 'id' => 456, 330 | 'name' => 'Sample Item 1', 331 | 'price' => 100, 332 | 'quantity' => 1, 333 | 'attributes' => array(), 334 | 'conditions' => [$itemCondition2, $itemCondition3] 335 | ); 336 | 337 | $this->cart->add($item); 338 | 339 | $this->assertEquals(85.00, $this->cart->get(456)->getPriceSumWithConditions(), 'Cart subtotal with 1 item should be 85'); 340 | $this->assertEquals(85.00, $this->cart->getSubTotal(), 'Cart subtotal with 1 item should be 85'); 341 | } 342 | 343 | public function test_add_item_condition() 344 | { 345 | $itemCondition2 = new CartCondition(array( 346 | 'name' => 'Item Gift Pack 25.00', 347 | 'type' => 'promo', 348 | 'value' => '-25', 349 | )); 350 | $coupon101 = new CartCondition(array( 351 | 'name' => 'COUPON 101', 352 | 'type' => 'coupon', 353 | 'value' => '-5%', 354 | )); 355 | 356 | $item = array( 357 | 'id' => 456, 358 | 'name' => 'Sample Item 1', 359 | 'price' => 100, 360 | 'quantity' => 1, 361 | 'attributes' => array(), 362 | 'conditions' => [$itemCondition2] 363 | ); 364 | 365 | $this->cart->add($item); 366 | 367 | // let's prove first we have 1 condition on this item 368 | $this->assertCount(1, $this->cart->get($item['id'])['conditions'], "Item should have 1 condition"); 369 | 370 | // now let's insert a condition on an existing item on the cart 371 | $this->cart->addItemCondition($item['id'], $coupon101); 372 | 373 | $this->assertCount(2, $this->cart->get($item['id'])['conditions'], "Item should have 2 conditions"); 374 | } 375 | 376 | public function test_add_item_condition_restrict_negative_price() 377 | { 378 | $condition = new CartCondition([ 379 | 'name' => 'Substract amount but prevent negative value', 380 | 'type' => 'promo', 381 | 'value' => '-25', 382 | ]); 383 | 384 | $item = [ 385 | 'id' => 789, 386 | 'name' => 'Sample Item 1', 387 | 'price' => 20, 388 | 'quantity' => 1, 389 | 'attributes' => [], 390 | 'conditions' => [ 391 | $condition, 392 | ] 393 | ]; 394 | 395 | $this->cart->add($item); 396 | 397 | // Since the product price is 20 and the condition reduces it by 25, 398 | // check that the item's price has been prevented from dropping below zero. 399 | $this->assertEquals(0.00, $this->cart->get($item['id'])->getPriceSumWithConditions(), "The item's price should be prevented from going below zero."); 400 | } 401 | 402 | public function test_get_cart_condition_by_condition_name() 403 | { 404 | $itemCondition1 = new CartCondition(array( 405 | 'name' => 'SALE 5%', 406 | 'type' => 'sale', 407 | 'target' => 'total', 408 | 'value' => '-5%', 409 | )); 410 | $itemCondition2 = new CartCondition(array( 411 | 'name' => 'Item Gift Pack 25.00', 412 | 'type' => 'promo', 413 | 'target' => 'total', 414 | 'value' => '-25', 415 | )); 416 | 417 | $item = array( 418 | 'id' => 456, 419 | 'name' => 'Sample Item 1', 420 | 'price' => 100, 421 | 'quantity' => 1, 422 | 'attributes' => array(), 423 | ); 424 | 425 | $this->cart->add($item); 426 | 427 | $this->cart->condition([$itemCondition1, $itemCondition2]); 428 | 429 | // get a condition applied on cart by condition name 430 | $condition = $this->cart->getCondition($itemCondition1->getName()); 431 | 432 | $this->assertEquals($condition->getName(), 'SALE 5%'); 433 | $this->assertEquals($condition->getTarget(), 'total'); 434 | $this->assertEquals($condition->getType(), 'sale'); 435 | $this->assertEquals($condition->getValue(), '-5%'); 436 | } 437 | 438 | public function test_remove_cart_condition_by_condition_name() 439 | { 440 | $itemCondition1 = new CartCondition(array( 441 | 'name' => 'SALE 5%', 442 | 'type' => 'sale', 443 | 'target' => 'total', 444 | 'value' => '-5%', 445 | )); 446 | $itemCondition2 = new CartCondition(array( 447 | 'name' => 'Item Gift Pack 25.00', 448 | 'type' => 'promo', 449 | 'target' => 'total', 450 | 'value' => '-25', 451 | )); 452 | 453 | $item = array( 454 | 'id' => 456, 455 | 'name' => 'Sample Item 1', 456 | 'price' => 100, 457 | 'quantity' => 1, 458 | 'attributes' => array(), 459 | ); 460 | 461 | $this->cart->add($item); 462 | 463 | $this->cart->condition([$itemCondition1, $itemCondition2]); 464 | 465 | // let's prove first we have now two conditions in the cart 466 | $this->assertEquals(2, $this->cart->getConditions()->count(), 'Cart should have two conditions'); 467 | 468 | // now let's remove a specific condition by condition name 469 | $this->cart->removeCartCondition('SALE 5%'); 470 | 471 | // cart should have now only 1 condition 472 | $this->assertEquals(1, $this->cart->getConditions()->count(), 'Cart should have one condition'); 473 | $this->assertEquals('Item Gift Pack 25.00', $this->cart->getConditions()->first()->getName()); 474 | } 475 | 476 | public function test_remove_item_condition_by_condition_name() 477 | { 478 | $itemCondition1 = new CartCondition(array( 479 | 'name' => 'SALE 5%', 480 | 'type' => 'sale', 481 | 'value' => '-5%', 482 | )); 483 | $itemCondition2 = new CartCondition(array( 484 | 'name' => 'Item Gift Pack 25.00', 485 | 'type' => 'promo', 486 | 'value' => '-25', 487 | )); 488 | 489 | $item = array( 490 | 'id' => 456, 491 | 'name' => 'Sample Item 1', 492 | 'price' => 100, 493 | 'quantity' => 1, 494 | 'attributes' => array(), 495 | 'conditions' => [$itemCondition1, $itemCondition2] 496 | ); 497 | 498 | $this->cart->add($item); 499 | 500 | // let's very first the item has 2 conditions in it 501 | $this->assertCount(2,$this->cart->get(456)['conditions'], 'Item should have two conditions'); 502 | 503 | // now let's remove a condition on that item using the condition name 504 | $this->cart->removeItemCondition(456, 'SALE 5%'); 505 | 506 | // now we should have only 1 condition left on that item 507 | $this->assertCount(1,$this->cart->get(456)['conditions'], 'Item should have one condition left'); 508 | } 509 | 510 | public function test_remove_item_condition_by_condition_name_scenario_two() 511 | { 512 | // NOTE: in this scenario, we will add the conditions not in array format 513 | 514 | $itemCondition = new CartCondition(array( 515 | 'name' => 'SALE 5%', 516 | 'type' => 'sale', 517 | 'value' => '-5%', 518 | )); 519 | 520 | $item = array( 521 | 'id' => 456, 522 | 'name' => 'Sample Item 1', 523 | 'price' => 100, 524 | 'quantity' => 1, 525 | 'attributes' => array(), 526 | 'conditions' => $itemCondition // <--not in array format 527 | ); 528 | 529 | $this->cart->add($item); 530 | 531 | // let's very first the item has 2 conditions in it 532 | $this->assertNotEmpty($this->cart->get(456)['conditions'], 'Item should have one condition in it.'); 533 | 534 | // now let's remove a condition on that item using the condition name 535 | $this->cart->removeItemCondition(456, 'SALE 5%'); 536 | 537 | // now we should have only 1 condition left on that item 538 | $this->assertEmpty($this->cart->get(456)['conditions'], 'Item should have no condition now'); 539 | } 540 | 541 | public function test_clear_item_conditions() 542 | { 543 | $itemCondition1 = new CartCondition(array( 544 | 'name' => 'SALE 5%', 545 | 'type' => 'sale', 546 | 'value' => '-5%', 547 | )); 548 | $itemCondition2 = new CartCondition(array( 549 | 'name' => 'Item Gift Pack 25.00', 550 | 'type' => 'promo', 551 | 'value' => '-25', 552 | )); 553 | 554 | $item = array( 555 | 'id' => 456, 556 | 'name' => 'Sample Item 1', 557 | 'price' => 100, 558 | 'quantity' => 1, 559 | 'attributes' => array(), 560 | 'conditions' => [$itemCondition1, $itemCondition2] 561 | ); 562 | 563 | $this->cart->add($item); 564 | 565 | // let's very first the item has 2 conditions in it 566 | $this->assertCount(2, $this->cart->get(456)['conditions'], 'Item should have two conditions'); 567 | 568 | // now let's remove all condition on that item 569 | $this->cart->clearItemConditions(456); 570 | 571 | // now we should have only 0 condition left on that item 572 | $this->assertCount(0, $this->cart->get(456)['conditions'], 'Item should have no conditions now'); 573 | } 574 | 575 | public function test_clear_cart_conditions() 576 | { 577 | // NOTE: 578 | // This only clears all conditions that has been added in a cart bases 579 | // this does not remove conditions on per item bases 580 | 581 | $itemCondition1 = new CartCondition(array( 582 | 'name' => 'SALE 5%', 583 | 'type' => 'sale', 584 | 'target' => 'total', 585 | 'value' => '-5%', 586 | )); 587 | $itemCondition2 = new CartCondition(array( 588 | 'name' => 'Item Gift Pack 25.00', 589 | 'type' => 'promo', 590 | 'target' => 'total', 591 | 'value' => '-25', 592 | )); 593 | 594 | $item = array( 595 | 'id' => 456, 596 | 'name' => 'Sample Item 1', 597 | 'price' => 100, 598 | 'quantity' => 1, 599 | 'attributes' => array(), 600 | ); 601 | 602 | $this->cart->add($item); 603 | 604 | $this->cart->condition([$itemCondition1, $itemCondition2]); 605 | 606 | // let's prove first we have now two conditions in the cart 607 | $this->assertEquals(2, $this->cart->getConditions()->count(), 'Cart should have two conditions'); 608 | 609 | // now let's clear cart conditions 610 | $this->cart->clearCartConditions(); 611 | 612 | // cart should have now only 1 condition 613 | $this->assertEquals(0, $this->cart->getConditions()->count(), 'Cart should have no conditions now'); 614 | } 615 | 616 | public function test_get_calculated_value_of_a_condition() 617 | { 618 | $cartCondition1 = new CartCondition(array( 619 | 'name' => 'SALE 5%', 620 | 'type' => 'sale', 621 | 'target' => 'total', 622 | 'value' => '-5%', 623 | )); 624 | $cartCondition2 = new CartCondition(array( 625 | 'name' => 'Item Gift Pack 25.00', 626 | 'type' => 'promo', 627 | 'target' => 'total', 628 | 'value' => '-25', 629 | )); 630 | 631 | $item = array( 632 | 'id' => 456, 633 | 'name' => 'Sample Item 1', 634 | 'price' => 100, 635 | 'quantity' => 1, 636 | 'attributes' => array(), 637 | ); 638 | 639 | $this->cart->add($item); 640 | 641 | $this->cart->condition([$cartCondition1, $cartCondition2]); 642 | 643 | $subTotal = $this->cart->getSubTotal(); 644 | 645 | $this->assertEquals(100, $subTotal, 'Subtotal should be 100'); 646 | 647 | // way 1 648 | // now we will get the calculated value of the condition 1 649 | $cond1 = $this->cart->getCondition('SALE 5%'); 650 | $this->assertEquals(5,$cond1->getCalculatedValue($subTotal), 'The calculated value must be 5'); 651 | 652 | // way 2 653 | // get all cart conditions and get their calculated values 654 | $conditions = $this->cart->getConditions(); 655 | $this->assertEquals(5, $conditions['SALE 5%']->getCalculatedValue($subTotal),'First condition calculated value must be 5'); 656 | $this->assertEquals(25, $conditions['Item Gift Pack 25.00']->getCalculatedValue($subTotal),'First condition calculated value must be 5'); 657 | } 658 | 659 | public function test_get_conditions_by_type() 660 | { 661 | $cartCondition1 = new CartCondition(array( 662 | 'name' => 'SALE 5%', 663 | 'type' => 'sale', 664 | 'target' => 'total', 665 | 'value' => '-5%', 666 | )); 667 | $cartCondition2 = new CartCondition(array( 668 | 'name' => 'Item Gift Pack 25.00', 669 | 'type' => 'promo', 670 | 'target' => 'total', 671 | 'value' => '-25', 672 | )); 673 | $cartCondition3 = new CartCondition(array( 674 | 'name' => 'Item Less 8%', 675 | 'type' => 'promo', 676 | 'target' => 'total', 677 | 'value' => '-8%', 678 | )); 679 | 680 | $item = array( 681 | 'id' => 456, 682 | 'name' => 'Sample Item 1', 683 | 'price' => 100, 684 | 'quantity' => 1, 685 | 'attributes' => array(), 686 | ); 687 | 688 | $this->cart->add($item); 689 | 690 | $this->cart->condition([$cartCondition1, $cartCondition2, $cartCondition3]); 691 | 692 | // now lets get all conditions added in the cart with the type "promo" 693 | $promoConditions = $this->cart->getConditionsByType('promo'); 694 | 695 | $this->assertEquals(2, $promoConditions->count(), "We should have 2 items as promo condition type."); 696 | } 697 | 698 | public function test_remove_conditions_by_type() 699 | { 700 | // NOTE: 701 | // when add a new condition, the condition's name will be the key to be use 702 | // to access the condition. For some reasons, if the condition name contains 703 | // a "dot" on it ("."), for example adding a condition with name "SALE 35.00" 704 | // this will cause issues when removing this condition by name, this will not be removed 705 | // so when adding a condition, the condition name should not contain any "period" (.) 706 | // to avoid any issues removing it using remove method: removeCartCondition($conditionName); 707 | 708 | $cartCondition1 = new CartCondition(array( 709 | 'name' => 'SALE 5%', 710 | 'type' => 'sale', 711 | 'target' => 'total', 712 | 'value' => '-5%', 713 | )); 714 | $cartCondition2 = new CartCondition(array( 715 | 'name' => 'Item Gift Pack 20', 716 | 'type' => 'promo', 717 | 'target' => 'total', 718 | 'value' => '-25', 719 | )); 720 | $cartCondition3 = new CartCondition(array( 721 | 'name' => 'Item Less 8%', 722 | 'type' => 'promo', 723 | 'target' => 'total', 724 | 'value' => '-8%', 725 | )); 726 | 727 | $item = array( 728 | 'id' => 456, 729 | 'name' => 'Sample Item 1', 730 | 'price' => 100, 731 | 'quantity' => 1, 732 | 'attributes' => array(), 733 | ); 734 | 735 | $this->cart->add($item); 736 | 737 | $this->cart->condition([$cartCondition1, $cartCondition2, $cartCondition3]); 738 | 739 | // now lets remove all conditions added in the cart with the type "promo" 740 | $this->cart->removeConditionsByType('promo'); 741 | 742 | $this->assertEquals(1, $this->cart->getConditions()->count(), "We should have 1 condition remaining as promo conditions type has been removed."); 743 | } 744 | 745 | public function test_add_cart_condition_without_condition_attributes() 746 | { 747 | $cartCondition1 = new CartCondition(array( 748 | 'name' => 'SALE 5%', 749 | 'type' => 'sale', 750 | 'target' => 'total', 751 | 'value' => '-5%' 752 | )); 753 | 754 | $item = array( 755 | 'id' => 456, 756 | 'name' => 'Sample Item 1', 757 | 'price' => 100, 758 | 'quantity' => 1, 759 | 'attributes' => array(), 760 | ); 761 | 762 | $this->cart->add($item); 763 | 764 | $this->cart->condition([$cartCondition1]); 765 | 766 | // prove first we have now the condition on the cart 767 | $contition = $this->cart->getCondition("SALE 5%"); 768 | $this->assertEquals('SALE 5%',$contition->getName()); 769 | 770 | // when get attribute is called and there is no attributes added, 771 | // it should return an empty array 772 | $conditionAttribute = $contition->getAttributes(); 773 | $this->assertIsArray($conditionAttribute); 774 | } 775 | 776 | public function test_add_cart_condition_with_condition_attributes() 777 | { 778 | $cartCondition1 = new CartCondition(array( 779 | 'name' => 'SALE 5%', 780 | 'type' => 'sale', 781 | 'target' => 'total', 782 | 'value' => '-5%', 783 | 'attributes' => array( 784 | 'description' => 'october fest promo sale', 785 | 'sale_start_date' => '2015-01-20', 786 | 'sale_end_date' => '2015-01-30', 787 | ) 788 | )); 789 | 790 | $item = array( 791 | 'id' => 456, 792 | 'name' => 'Sample Item 1', 793 | 'price' => 100, 794 | 'quantity' => 1, 795 | 'attributes' => array(), 796 | ); 797 | 798 | $this->cart->add($item); 799 | 800 | $this->cart->condition([$cartCondition1]); 801 | 802 | // prove first we have now the condition on the cart 803 | $contition = $this->cart->getCondition("SALE 5%"); 804 | $this->assertEquals('SALE 5%',$contition->getName()); 805 | 806 | // when get attribute is called and there is no attributes added, 807 | // it should return an empty array 808 | $conditionAttributes = $contition->getAttributes(); 809 | $this->assertIsArray($conditionAttributes); 810 | $this->assertArrayHasKey('description',$conditionAttributes); 811 | $this->assertArrayHasKey('sale_start_date',$conditionAttributes); 812 | $this->assertArrayHasKey('sale_end_date',$conditionAttributes); 813 | $this->assertEquals('october fest promo sale',$conditionAttributes['description']); 814 | $this->assertEquals('2015-01-20',$conditionAttributes['sale_start_date']); 815 | $this->assertEquals('2015-01-30',$conditionAttributes['sale_end_date']); 816 | } 817 | 818 | public function test_get_order_from_condition() 819 | { 820 | $cartCondition1 = new CartCondition(array( 821 | 'name' => 'SALE 5%', 822 | 'type' => 'sale', 823 | 'target' => 'total', 824 | 'value' => '-5%', 825 | 'order' => 2 826 | )); 827 | $cartCondition2 = new CartCondition(array( 828 | 'name' => 'Item Gift Pack 20', 829 | 'type' => 'promo', 830 | 'target' => 'total', 831 | 'value' => '-25', 832 | 'order' => '3' 833 | )); 834 | $cartCondition3 = new CartCondition(array( 835 | 'name' => 'Item Less 8%', 836 | 'type' => 'tax', 837 | 'target' => 'total', 838 | 'value' => '-8%', 839 | 'order' => 'first' 840 | )); 841 | 842 | $this->assertEquals(2, $cartCondition1->getOrder()); 843 | $this->assertEquals(3, $cartCondition2->getOrder()); // numeric string is converted to integer 844 | $this->assertEquals(0, $cartCondition3->getOrder()); // no numeric string is converted to 0 845 | 846 | $this->cart->condition($cartCondition1); 847 | $this->cart->condition($cartCondition2); 848 | $this->cart->condition($cartCondition3); 849 | 850 | $conditions = $this->cart->getConditions(); 851 | 852 | $this->assertEquals('sale', $conditions->shift()->getType()); 853 | $this->assertEquals('promo', $conditions->shift()->getType()); 854 | $this->assertEquals('tax', $conditions->shift()->getType()); 855 | } 856 | 857 | public function test_condition_ordering() 858 | { 859 | $cartCondition1 = new CartCondition(array( 860 | 'name' => 'TAX', 861 | 'type' => 'tax', 862 | 'target' => 'total', 863 | 'value' => '-8%', 864 | 'order' => 5 865 | )); 866 | $cartCondition2 = new CartCondition(array( 867 | 'name' => 'SALE 5%', 868 | 'type' => 'sale', 869 | 'target' => 'total', 870 | 'value' => '-5%', 871 | 'order' => 2 872 | )); 873 | $cartCondition3 = new CartCondition(array( 874 | 'name' => 'Item Gift Pack 20', 875 | 'type' => 'promo', 876 | 'target' => 'total', 877 | 'value' => '-25', 878 | 'order' => 1 879 | )); 880 | 881 | $this->fillCart(); 882 | 883 | $this->cart->condition($cartCondition1); 884 | $this->cart->condition($cartCondition2); 885 | $this->cart->condition($cartCondition3); 886 | 887 | $this->assertEquals('Item Gift Pack 20',$this->cart->getConditions()->first()->getName()); 888 | $this->assertEquals('TAX',$this->cart->getConditions()->last()->getName()); 889 | } 890 | 891 | protected function fillCart() 892 | { 893 | $items = array( 894 | array( 895 | 'id' => 456, 896 | 'name' => 'Sample Item 1', 897 | 'price' => 67.99, 898 | 'quantity' => 1, 899 | 'attributes' => array() 900 | ), 901 | array( 902 | 'id' => 568, 903 | 'name' => 'Sample Item 2', 904 | 'price' => 69.25, 905 | 'quantity' => 1, 906 | 'attributes' => array() 907 | ), 908 | array( 909 | 'id' => 856, 910 | 'name' => 'Sample Item 3', 911 | 'price' => 50.25, 912 | 'quantity' => 1, 913 | 'attributes' => array() 914 | ), 915 | ); 916 | 917 | $this->cart->add($items); 918 | } 919 | } 920 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel 5 & 6 , 7 & 9 Shopping Cart 2 | [![Build Status](https://travis-ci.org/darryldecode/laravelshoppingcart.svg?branch=master)](https://travis-ci.org/darryldecode/laravelshoppingcart) 3 | [![Total Downloads](https://poser.pugx.org/darryldecode/cart/d/total.svg)](https://packagist.org/packages/darryldecode/cart) 4 | [![License](https://poser.pugx.org/darryldecode/cart/license.svg)](https://packagist.org/packages/darryldecode/cart) 5 | 6 | A Shopping Cart Implementation for Laravel Framework 7 | 8 | ## QUICK PARTIAL DEMO 9 | 10 | Demo: https://shoppingcart-demo.darrylfernandez.com/cart 11 | 12 | Git repo of the demo: https://github.com/darryldecode/laravelshoppingcart-demo 13 | 14 | ## INSTALLATION 15 | 16 | Install the package through [Composer](http://getcomposer.org/). 17 | 18 | For Laravel 5.1~: 19 | `composer require "darryldecode/cart:~2.0"` 20 | 21 | For Laravel 5.5, 5.6, or 5.7~, 9: 22 | 23 | ```composer require "darryldecode/cart:~4.0"``` or 24 | ```composer require "darryldecode/cart"``` 25 | 26 | ## CONFIGURATION 27 | 28 | 1. Open config/app.php and add this line to your Service Providers Array. 29 | 30 | ```php 31 | Darryldecode\Cart\CartServiceProvider::class 32 | ``` 33 | 34 | 2. Open config/app.php and add this line to your Aliases 35 | 36 | ```php 37 | 'Cart' => Darryldecode\Cart\Facades\CartFacade::class 38 | ``` 39 | 40 | 3. Optional configuration file (useful if you plan to have full control) 41 | 42 | ```php 43 | php artisan vendor:publish --provider="Darryldecode\Cart\CartServiceProvider" --tag="config" 44 | ``` 45 | 46 | ## HOW TO USE 47 | 48 | - [Quick Usage](#usage-usage-example) 49 | - [Usage](#usage) 50 | - [Conditions](#conditions) 51 | - [Items](#items) 52 | - [Associating Models](#associating-models) 53 | - [Instances](#instances) 54 | - [Exceptions](#exceptions) 55 | - [Events](#events) 56 | - [Format Response](#format) 57 | - [Examples](#examples) 58 | - [Using Different Storage](#storage) 59 | - [License](#license) 60 | 61 | ## Quick Usage Example 62 | 63 | ```php 64 | // Quick Usage with the Product Model Association & User session binding 65 | 66 | $Product = Product::find($productId); // assuming you have a Product model with id, name, description & price 67 | $rowId = 456; // generate a unique() row ID 68 | $userID = 2; // the user ID to bind the cart contents 69 | 70 | // add the product to cart 71 | \Cart::session($userID)->add(array( 72 | 'id' => $rowId, 73 | 'name' => $Product->name, 74 | 'price' => $Product->price, 75 | 'quantity' => 4, 76 | 'attributes' => array(), 77 | 'associatedModel' => $Product 78 | )); 79 | 80 | // update the item on cart 81 | \Cart::session($userID)->update($rowId,[ 82 | 'quantity' => 2, 83 | 'price' => 98.67 84 | ]); 85 | 86 | // delete an item on cart 87 | \Cart::session($userID)->remove($rowId); 88 | 89 | // view the cart items 90 | $items = \Cart::getContent(); 91 | foreach($items as $row) { 92 | 93 | echo $row->id; // row ID 94 | echo $row->name; 95 | echo $row->qty; 96 | echo $row->price; 97 | 98 | echo $item->associatedModel->id; // whatever properties your model have 99 | echo $item->associatedModel->name; // whatever properties your model have 100 | echo $item->associatedModel->description; // whatever properties your model have 101 | } 102 | 103 | // FOR FULL USAGE, SEE BELOW.. 104 | ``` 105 | 106 | ## Usage 107 | 108 | ### IMPORTANT NOTE! 109 | 110 | By default, the cart has a default sessionKey that holds the cart data. This 111 | also serves as a cart unique identifier which you can use to bind a cart to a specific user. 112 | To override this default session Key, you will just simply call the `\Cart::session($sessionKey)` method 113 | BEFORE ANY OTHER METHODS!!. 114 | 115 | Example: 116 | 117 | ```php 118 | $userId // the current login user id 119 | 120 | // This tells the cart that we only need or manipulate 121 | // the cart data of a specific user. It doesn't need to be $userId, 122 | // you can use any unique key that represents a unique to a user or customer. 123 | // basically this binds the cart to a specific user. 124 | \Cart::session($userId); 125 | 126 | // then followed by the normal cart usage 127 | \Cart::add(); 128 | \Cart::update(); 129 | \Cart::remove(); 130 | \Cart::condition($condition1); 131 | \Cart::getTotal(); 132 | \Cart::getSubTotal(); 133 | \Cart::getSubTotalWithoutConditions(); 134 | \Cart::addItemCondition($productID, $coupon101); 135 | // and so on.. 136 | ``` 137 | 138 | See More Examples below: 139 | 140 | Adding Item on Cart: **Cart::add()** 141 | 142 | There are several ways you can add items on your cart, see below: 143 | 144 | ```php 145 | /** 146 | * add item to the cart, it can be an array or multi dimensional array 147 | * 148 | * @param string|array $id 149 | * @param string $name 150 | * @param float $price 151 | * @param int $quantity 152 | * @param array $attributes 153 | * @param CartCondition|array $conditions 154 | * @return $this 155 | * @throws InvalidItemException 156 | */ 157 | 158 | # ALWAYS REMEMBER TO BIND THE CART TO A USER BEFORE CALLING ANY CART FUNCTION 159 | # SO CART WILL KNOW WHO'S CART DATA YOU WANT TO MANIPULATE. SEE IMPORTANT NOTICE ABOVE. 160 | # EXAMPLE: \Cart::session($userId); then followed by cart normal usage. 161 | 162 | # NOTE: 163 | # the 'id' field in adding a new item on cart is not intended for the Model ID (example Product ID) 164 | # instead make sure to put a unique ID for every unique product or product that has it's own unique prirce, 165 | # because it is used for updating cart and how each item on cart are segregated during calculation and quantities. 166 | # You can put the model_id instead as an attribute for full flexibility. 167 | # Example is that if you want to add same products on the cart but with totally different attribute and price. 168 | # If you use the Product's ID as the 'id' field in cart, it will result to increase in quanity instead 169 | # of adding it as a unique product with unique attribute and price. 170 | 171 | // Simplest form to add item on your cart 172 | Cart::add(455, 'Sample Item', 100.99, 2, array()); 173 | 174 | // array format 175 | Cart::add(array( 176 | 'id' => 456, // inique row ID 177 | 'name' => 'Sample Item', 178 | 'price' => 67.99, 179 | 'quantity' => 4, 180 | 'attributes' => array() 181 | )); 182 | 183 | // add multiple items at one time 184 | Cart::add(array( 185 | array( 186 | 'id' => 456, 187 | 'name' => 'Sample Item 1', 188 | 'price' => 67.99, 189 | 'quantity' => 4, 190 | 'attributes' => array() 191 | ), 192 | array( 193 | 'id' => 568, 194 | 'name' => 'Sample Item 2', 195 | 'price' => 69.25, 196 | 'quantity' => 4, 197 | 'attributes' => array( 198 | 'size' => 'L', 199 | 'color' => 'blue' 200 | ) 201 | ), 202 | )); 203 | 204 | // add cart items to a specific user 205 | $userId = auth()->user()->id; // or any string represents user identifier 206 | Cart::session($userId)->add(array( 207 | 'id' => 456, // inique row ID 208 | 'name' => 'Sample Item', 209 | 'price' => 67.99, 210 | 'quantity' => 4, 211 | 'attributes' => array(), 212 | 'associatedModel' => $Product 213 | )); 214 | 215 | // NOTE: 216 | // Please keep in mind that when adding an item on cart, the "id" should be unique as it serves as 217 | // row identifier as well. If you provide same ID, it will assume the operation will be an update to its quantity 218 | // to avoid cart item duplicates 219 | ``` 220 | 221 | Updating an item on a cart: **Cart::update()** 222 | 223 | Updating an item on a cart is very simple: 224 | 225 | ```php 226 | /** 227 | * update a cart 228 | * 229 | * @param $id (the item ID) 230 | * @param array $data 231 | * 232 | * the $data will be an associative array, you don't need to pass all the data, only the key value 233 | * of the item you want to update on it 234 | */ 235 | 236 | Cart::update(456, array( 237 | 'name' => 'New Item Name', // new item name 238 | 'price' => 98.67, // new item price, price can also be a string format like so: '98.67' 239 | )); 240 | 241 | // you may also want to update a product's quantity 242 | Cart::update(456, array( 243 | 'quantity' => 2, // so if the current product has a quantity of 4, another 2 will be added so this will result to 6 244 | )); 245 | 246 | // you may also want to update a product by reducing its quantity, you do this like so: 247 | Cart::update(456, array( 248 | 'quantity' => -1, // so if the current product has a quantity of 4, it will subtract 1 and will result to 3 249 | )); 250 | 251 | // NOTE: as you can see by default, the quantity update is relative to its current value 252 | // if you want to just totally replace the quantity instead of incrementing or decrementing its current quantity value 253 | // you can pass an array in quantity value like so: 254 | Cart::update(456, array( 255 | 'quantity' => array( 256 | 'relative' => false, 257 | 'value' => 5 258 | ), 259 | )); 260 | // so with that code above as relative is flagged as false, if the item's quantity before is 2 it will now be 5 instead of 261 | // 5 + 2 which results to 7 if updated relatively.. 262 | 263 | // updating a cart for a specific user 264 | $userId = auth()->user()->id; // or any string represents user identifier 265 | Cart::session($userId)->update(456, array( 266 | 'name' => 'New Item Name', // new item name 267 | 'price' => 98.67, // new item price, price can also be a string format like so: '98.67' 268 | )); 269 | ``` 270 | 271 | Removing an item on a cart: **Cart::remove()** 272 | 273 | Removing an item on a cart is very easy: 274 | 275 | ```php 276 | /** 277 | * removes an item on cart by item ID 278 | * 279 | * @param $id 280 | */ 281 | 282 | Cart::remove(456); 283 | 284 | // removing cart item for a specific user's cart 285 | $userId = auth()->user()->id; // or any string represents user identifier 286 | Cart::session($userId)->remove(456); 287 | ``` 288 | 289 | Getting an item on a cart: **Cart::get()** 290 | 291 | ```php 292 | 293 | /** 294 | * get an item on a cart by item ID 295 | * if item ID is not found, this will return null 296 | * 297 | * @param $itemId 298 | * @return null|array 299 | */ 300 | 301 | $itemId = 456; 302 | 303 | Cart::get($itemId); 304 | 305 | // You can also get the sum of the Item multiplied by its quantity, see below: 306 | $summedPrice = Cart::get($itemId)->getPriceSum(); 307 | 308 | // get an item on a cart by item ID for a specific user's cart 309 | $userId = auth()->user()->id; // or any string represents user identifier 310 | Cart::session($userId)->get($itemId); 311 | ``` 312 | 313 | Getting cart's contents and count: **Cart::getContent()** 314 | 315 | ```php 316 | 317 | /** 318 | * get the cart 319 | * 320 | * @return CartCollection 321 | */ 322 | 323 | $cartCollection = Cart::getContent(); 324 | 325 | // NOTE: Because cart collection extends Laravel's Collection 326 | // You can use methods you already know about Laravel's Collection 327 | // See some of its method below: 328 | 329 | // count carts contents 330 | $cartCollection->count(); 331 | 332 | // transformations 333 | $cartCollection->toArray(); 334 | $cartCollection->toJson(); 335 | 336 | // Getting cart's contents for a specific user 337 | $userId = auth()->user()->id; // or any string represents user identifier 338 | Cart::session($userId)->getContent($itemId); 339 | ``` 340 | 341 | Check if cart is empty: **Cart::isEmpty()** 342 | 343 | ```php 344 | /** 345 | * check if cart is empty 346 | * 347 | * @return bool 348 | */ 349 | Cart::isEmpty(); 350 | 351 | // Check if cart's contents is empty for a specific user 352 | $userId = auth()->user()->id; // or any string represents user identifier 353 | Cart::session($userId)->isEmpty(); 354 | ``` 355 | 356 | Get cart total quantity: **Cart::getTotalQuantity()** 357 | 358 | ```php 359 | /** 360 | * get total quantity of items in the cart 361 | * 362 | * @return int 363 | */ 364 | $cartTotalQuantity = Cart::getTotalQuantity(); 365 | 366 | // for a specific user 367 | $cartTotalQuantity = Cart::session($userId)->getTotalQuantity(); 368 | ``` 369 | 370 | Get cart subtotal: **Cart::getSubTotal()** 371 | 372 | ```php 373 | /** 374 | * get cart sub total 375 | * 376 | * @return float 377 | */ 378 | $subTotal = Cart::getSubTotal(); 379 | 380 | // for a specific user 381 | $subTotal = Cart::session($userId)->getSubTotal(); 382 | ``` 383 | 384 | Get cart subtotal with out conditions: **Cart::getSubTotalWithoutConditions()** 385 | 386 | ```php 387 | /** 388 | * get cart sub total with out conditions 389 | * 390 | * @param bool $formatted 391 | * @return float 392 | */ 393 | $subTotalWithoutConditions = Cart::getSubTotalWithoutConditions(); 394 | 395 | // for a specific user 396 | $subTotalWithoutConditions = Cart::session($userId)->getSubTotalWithoutConditions(); 397 | ``` 398 | 399 | Get cart total: **Cart::getTotal()** 400 | 401 | ```php 402 | /** 403 | * the new total in which conditions are already applied 404 | * 405 | * @return float 406 | */ 407 | $total = Cart::getTotal(); 408 | 409 | // for a specific user 410 | $total = Cart::session($userId)->getTotal(); 411 | ``` 412 | 413 | Clearing the Cart: **Cart::clear()** 414 | 415 | ```php 416 | /** 417 | * clear cart 418 | * 419 | * @return void 420 | */ 421 | Cart::clear(); 422 | Cart::session($userId)->clear(); 423 | ``` 424 | 425 | ## Conditions 426 | 427 | Laravel Shopping Cart supports cart conditions. 428 | Conditions are very useful in terms of (coupons,discounts,sale,per-item sale and discounts etc.) 429 | See below carefully on how to use conditions. 430 | 431 | Conditions can be added on: 432 | 433 | 1.) Whole Cart Value bases 434 | 435 | 2.) Per-Item Bases 436 | 437 | First let's add a condition on a Cart Bases: 438 | 439 | There are also several ways of adding a condition on a cart: 440 | NOTE: 441 | 442 | When adding a condition on a cart bases, the 'target' should have value of 'subtotal' or 'total'. 443 | If the target is "subtotal" then this condition will be applied to subtotal. 444 | If the target is "total" then this condition will be applied to total. 445 | The order of operation also during calculation will vary on the order you have added the conditions. 446 | 447 | Also, when adding conditions, the 'value' field will be the bases of calculation. You can change this order 448 | by adding 'order' parameter in CartCondition. 449 | 450 | ```php 451 | 452 | // add single condition on a cart bases 453 | $condition = new \Darryldecode\Cart\CartCondition(array( 454 | 'name' => 'VAT 12.5%', 455 | 'type' => 'tax', 456 | 'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called. 457 | 'value' => '12.5%', 458 | 'attributes' => array( // attributes field is optional 459 | 'description' => 'Value added tax', 460 | 'more_data' => 'more data here' 461 | ) 462 | )); 463 | 464 | Cart::condition($condition); 465 | Cart::session($userId)->condition($condition); // for a speicifc user's cart 466 | 467 | // or add multiple conditions from different condition instances 468 | $condition1 = new \Darryldecode\Cart\CartCondition(array( 469 | 'name' => 'VAT 12.5%', 470 | 'type' => 'tax', 471 | 'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called. 472 | 'value' => '12.5%', 473 | 'order' => 2 474 | )); 475 | $condition2 = new \Darryldecode\Cart\CartCondition(array( 476 | 'name' => 'Express Shipping $15', 477 | 'type' => 'shipping', 478 | 'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called. 479 | 'value' => '+15', 480 | 'order' => 1 481 | )); 482 | Cart::condition($condition1); 483 | Cart::condition($condition2); 484 | 485 | // Note that after adding conditions that are targeted to be applied on subtotal, the result on getTotal() 486 | // will also be affected as getTotal() depends in getSubTotal() which is the subtotal. 487 | 488 | // add condition to only apply on totals, not in subtotal 489 | $condition = new \Darryldecode\Cart\CartCondition(array( 490 | 'name' => 'Express Shipping $15', 491 | 'type' => 'shipping', 492 | 'target' => 'total', // this condition will be applied to cart's total when getTotal() is called. 493 | 'value' => '+15', 494 | 'order' => 1 // the order of calculation of cart base conditions. The bigger the later to be applied. 495 | )); 496 | Cart::condition($condition); 497 | 498 | // The property 'order' lets you control the sequence of conditions when calculated. Also it lets you add different conditions through for example a shopping process with multiple 499 | // pages and still be able to set an order to apply the conditions. If no order is defined defaults to 0 500 | 501 | // NOTE!! On current version, 'order' parameter is only applicable for conditions for cart bases. It does not support on per item conditions. 502 | 503 | // or add multiple conditions as array 504 | Cart::condition([$condition1, $condition2]); 505 | 506 | // To get all applied conditions on a cart, use below: 507 | $cartConditions = Cart::getConditions(); 508 | foreach($cartConditions as $condition) 509 | { 510 | $condition->getTarget(); // the target of which the condition was applied 511 | $condition->getName(); // the name of the condition 512 | $condition->getType(); // the type 513 | $condition->getValue(); // the value of the condition 514 | $condition->getOrder(); // the order of the condition 515 | $condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added 516 | } 517 | 518 | // You can also get a condition that has been applied on the cart by using its name, use below: 519 | $condition = Cart::getCondition('VAT 12.5%'); 520 | $condition->getTarget(); // the target of which the condition was applied 521 | $condition->getName(); // the name of the condition 522 | $condition->getType(); // the type 523 | $condition->getValue(); // the value of the condition 524 | $condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added 525 | 526 | // You can get the conditions calculated value by providing the subtotal, see below: 527 | $subTotal = Cart::getSubTotal(); 528 | $condition = Cart::getCondition('VAT 12.5%'); 529 | $conditionCalculatedValue = $condition->getCalculatedValue($subTotal); 530 | ``` 531 | 532 | > NOTE: All cart based conditions should be added to cart's conditions before calling **Cart::getTotal()** 533 | > and if there are also conditions that are targeted to be applied to subtotal, it should be added to cart's conditions 534 | > before calling **Cart::getSubTotal()** 535 | 536 | ```php 537 | $cartTotal = Cart::getSubTotal(); // the subtotal with the conditions targeted to "subtotal" applied 538 | $cartTotal = Cart::getTotal(); // the total with the conditions targeted to "total" applied 539 | $cartTotal = Cart::session($userId)->getSubTotal(); // for a specific user's cart 540 | $cartTotal = Cart::session($userId)->getTotal(); // for a specific user's cart 541 | ``` 542 | 543 | Next is the Condition on Per-Item Bases. 544 | 545 | This is very useful if you have coupons to be applied specifically on an item and not on the whole cart value. 546 | 547 | > NOTE: When adding a condition on a per-item bases, the 'target' parameter is not needed or can be omitted. 548 | > unlike when adding conditions or per cart bases. 549 | 550 | Now let's add condition on an item. 551 | 552 | ```php 553 | 554 | // lets create first our condition instance 555 | $saleCondition = new \Darryldecode\Cart\CartCondition(array( 556 | 'name' => 'SALE 5%', 557 | 'type' => 'tax', 558 | 'value' => '-5%', 559 | )); 560 | 561 | // now the product to be added on cart 562 | $product = array( 563 | 'id' => 456, 564 | 'name' => 'Sample Item 1', 565 | 'price' => 100, 566 | 'quantity' => 1, 567 | 'attributes' => array(), 568 | 'conditions' => $saleCondition 569 | ); 570 | 571 | // finally add the product on the cart 572 | Cart::add($product); 573 | 574 | // you may also add multiple condition on an item 575 | $itemCondition1 = new \Darryldecode\Cart\CartCondition(array( 576 | 'name' => 'SALE 5%', 577 | 'type' => 'sale', 578 | 'value' => '-5%', 579 | )); 580 | $itemCondition2 = new CartCondition(array( 581 | 'name' => 'Item Gift Pack 25.00', 582 | 'type' => 'promo', 583 | 'value' => '-25', 584 | )); 585 | $itemCondition3 = new \Darryldecode\Cart\CartCondition(array( 586 | 'name' => 'MISC', 587 | 'type' => 'misc', 588 | 'value' => '+10', 589 | )); 590 | 591 | $item = array( 592 | 'id' => 456, 593 | 'name' => 'Sample Item 1', 594 | 'price' => 100, 595 | 'quantity' => 1, 596 | 'attributes' => array(), 597 | 'conditions' => [$itemCondition1, $itemCondition2, $itemCondition3] 598 | ); 599 | 600 | Cart::add($item); 601 | ``` 602 | 603 | > NOTE: All cart per-item conditions should be added before calling **Cart::getSubTotal()** 604 | 605 | Then Finally you can call **Cart::getSubTotal()** to get the Cart sub total with the applied conditions on each of the items. 606 | 607 | ```php 608 | // the subtotal will be calculated based on the conditions added that has target => "subtotal" 609 | // and also conditions that are added on per item 610 | $cartSubTotal = Cart::getSubTotal(); 611 | ``` 612 | 613 | Add condition to existing Item on the cart: **Cart::addItemCondition($productId, $itemCondition)** 614 | 615 | Adding Condition to an existing Item on the cart is simple as well. 616 | 617 | This is very useful when adding new conditions on an item during checkout process like coupons and promo codes. 618 | Let's see the example how to do it: 619 | 620 | ```php 621 | $productID = 456; 622 | $coupon101 = new CartCondition(array( 623 | 'name' => 'COUPON 101', 624 | 'type' => 'coupon', 625 | 'value' => '-5%', 626 | )); 627 | 628 | Cart::addItemCondition($productID, $coupon101); 629 | ``` 630 | 631 | Clearing Cart Conditions: **Cart::clearCartConditions()** 632 | 633 | ```php 634 | /** 635 | * clears all conditions on a cart, 636 | * this does not remove conditions that has been added specifically to an item/product. 637 | * If you wish to remove a specific condition to a product, you may use the method: removeItemCondition($itemId,$conditionName) 638 | * 639 | * @return void 640 | */ 641 | Cart::clearCartConditions() 642 | ``` 643 | 644 | Remove Specific Cart Condition: **Cart::removeCartCondition(\$conditionName)** 645 | 646 | ```php 647 | /** 648 | * removes a condition on a cart by condition name, 649 | * this can only remove conditions that are added on cart bases not conditions that are added on an item/product. 650 | * If you wish to remove a condition that has been added for a specific item/product, you may 651 | * use the removeItemCondition(itemId, conditionName) method instead. 652 | * 653 | * @param $conditionName 654 | * @return void 655 | */ 656 | $conditionName = 'Summer Sale 5%'; 657 | 658 | Cart::removeCartCondition($conditionName) 659 | ``` 660 | 661 | Remove Specific Item Condition: **Cart::removeItemCondition($itemId, $conditionName)** 662 | 663 | ```php 664 | /** 665 | * remove a condition that has been applied on an item that is already on the cart 666 | * 667 | * @param $itemId 668 | * @param $conditionName 669 | * @return bool 670 | */ 671 | Cart::removeItemCondition($itemId, $conditionName) 672 | ``` 673 | 674 | Clear all Item Conditions: **Cart::clearItemConditions(\$itemId)** 675 | 676 | ```php 677 | /** 678 | * remove all conditions that has been applied on an item that is already on the cart 679 | * 680 | * @param $itemId 681 | * @return bool 682 | */ 683 | Cart::clearItemConditions($itemId) 684 | ``` 685 | 686 | Get conditions by type: **Cart::getConditionsByType(\$type)** 687 | 688 | ```php 689 | /** 690 | * Get all the condition filtered by Type 691 | * Please Note that this will only return condition added on cart bases, not those conditions added 692 | * specifically on an per item bases 693 | * 694 | * @param $type 695 | * @return CartConditionCollection 696 | */ 697 | public function getConditionsByType($type) 698 | ``` 699 | 700 | Remove conditions by type: **Cart::removeConditionsByType(\$type)** 701 | 702 | ```php 703 | /** 704 | * Remove all the condition with the $type specified 705 | * Please Note that this will only remove condition added on cart bases, not those conditions added 706 | * specifically on an per item bases 707 | * 708 | * @param $type 709 | * @return $this 710 | */ 711 | public function removeConditionsByType($type) 712 | ``` 713 | 714 | ## Items 715 | 716 | The method **Cart::getContent()** returns a collection of items. 717 | 718 | To get the id of an item, use the property **\$item->id**. 719 | 720 | To get the name of an item, use the property **\$item->name**. 721 | 722 | To get the quantity of an item, use the property **\$item->quantity**. 723 | 724 | To get the attributes of an item, use the property **\$item->attributes**. 725 | 726 | To get the price of a single item without the conditions applied, use the property **\$item->price**. 727 | 728 | To get the subtotal of an item without the conditions applied, use the method **\$item->getPriceSum()**. 729 | 730 | ```php 731 | /** 732 | * get the sum of price 733 | * 734 | * @return mixed|null 735 | */ 736 | public function getPriceSum() 737 | 738 | ``` 739 | 740 | To get the price of a single item without the conditions applied, use the method 741 | 742 | **\$item->getPriceWithConditions()**. 743 | 744 | ```php 745 | /** 746 | * get the single price in which conditions are already applied 747 | * 748 | * @return mixed|null 749 | */ 750 | public function getPriceWithConditions() 751 | 752 | ``` 753 | 754 | To get the subtotal of an item with the conditions applied, use the method 755 | 756 | **\$item->getPriceSumWithConditions()** 757 | 758 | ```php 759 | /** 760 | * get the sum of price in which conditions are already applied 761 | * 762 | * @return mixed|null 763 | */ 764 | public function getPriceSumWithConditions() 765 | 766 | ``` 767 | 768 | **NOTE**: When you get price with conditions applied, only the conditions assigned to the current item will be calculated. 769 | Cart conditions won't be applied to price. 770 | 771 | ## Associating Models 772 | 773 | One can associate a cart item to a model. Let's say you have a `Product` model in your application. With the `associate()` method, you can tell the cart that an item in the cart, is associated to the `Product` model. 774 | 775 | That way you can access your model using the property **\$item->model**. 776 | 777 | Here is an example: 778 | 779 | ```php 780 | 781 | // add the item to the cart. 782 | $cartItem = Cart::add(455, 'Sample Item', 100.99, 2, array())->associate('Product'); 783 | 784 | // array format 785 | Cart::add(array( 786 | 'id' => 456, 787 | 'name' => 'Sample Item', 788 | 'price' => 67.99, 789 | 'quantity' => 4, 790 | 'attributes' => array(), 791 | 'associatedModel' => 'Product' 792 | )); 793 | 794 | // add multiple items at one time 795 | Cart::add(array( 796 | array( 797 | 'id' => 456, 798 | 'name' => 'Sample Item 1', 799 | 'price' => 67.99, 800 | 'quantity' => 4, 801 | 'attributes' => array(), 802 | 'associatedModel' => 'Product' 803 | ), 804 | array( 805 | 'id' => 568, 806 | 'name' => 'Sample Item 2', 807 | 'price' => 69.25, 808 | 'quantity' => 4, 809 | 'attributes' => array( 810 | 'size' => 'L', 811 | 'color' => 'blue' 812 | ), 813 | 'associatedModel' => 'Product' 814 | ), 815 | )); 816 | 817 | // Now, when iterating over the content of the cart, you can access the model. 818 | foreach(Cart::getContent() as $row) { 819 | echo 'You have ' . $row->qty . ' items of ' . $row->model->name . ' with description: "' . $row->model->description . '" in your cart.'; 820 | } 821 | ``` 822 | 823 | **NOTE**: This only works when adding an item to cart. 824 | 825 | ## Instances 826 | 827 | You may also want multiple cart instances on the same page without conflicts. 828 | To do that, 829 | 830 | Create a new Service Provider and then on register() method, you can put this like so: 831 | 832 | ```php 833 | $this->app['wishlist'] = $this->app->share(function($app) 834 | { 835 | $storage = $app['session']; // laravel session storage 836 | $events = $app['events']; // laravel event handler 837 | $instanceName = 'wishlist'; // your cart instance name 838 | $session_key = 'AsASDMCks0ks1'; // your unique session key to hold cart items 839 | 840 | return new Cart( 841 | $storage, 842 | $events, 843 | $instanceName, 844 | $session_key 845 | ); 846 | }); 847 | 848 | // for 5.4 or newer 849 | use Darryldecode\Cart\Cart; 850 | use Illuminate\Support\ServiceProvider; 851 | 852 | class WishListProvider extends ServiceProvider 853 | { 854 | /** 855 | * Bootstrap the application services. 856 | * 857 | * @return void 858 | */ 859 | public function boot() 860 | { 861 | // 862 | } 863 | /** 864 | * Register the application services. 865 | * 866 | * @return void 867 | */ 868 | public function register() 869 | { 870 | $this->app->singleton('wishlist', function($app) 871 | { 872 | $storage = $app['session']; 873 | $events = $app['events']; 874 | $instanceName = 'cart_2'; 875 | $session_key = '88uuiioo99888'; 876 | return new Cart( 877 | $storage, 878 | $events, 879 | $instanceName, 880 | $session_key, 881 | config('shopping_cart') 882 | ); 883 | }); 884 | } 885 | } 886 | ``` 887 | 888 | IF you are having problem with multiple cart instance, please see the codes on 889 | this demo repo here: [DEMO](https://github.com/darryldecode/laravelshoppingcart-demo) 890 | 891 | ## Exceptions 892 | 893 | There are currently only two exceptions. 894 | 895 | | Exception | Description | 896 | | --------------------------- | ------------------------------------------------------------------------- | 897 | | _InvalidConditionException_ | When there is an invalid field value during instantiating a new Condition | 898 | | _InvalidItemException_ | When a new product has invalid field values (id,name,price,quantity) | 899 | | _UnknownModelException_ | When you try to associate a none existing model to a cart item. | 900 | 901 | ## Events 902 | 903 | The cart has currently 9 events you can listen and hook some actons. 904 | 905 | | Event | Fired | 906 | | ---------------------------- | -------------------------------------- | 907 | | cart.created(\$cart) | When a cart is instantiated | 908 | | cart.adding($items, $cart) | When an item is attempted to be added | 909 | | cart.added($items, $cart) | When an item is added on cart | 910 | | cart.updating($items, $cart) | When an item is being updated | 911 | | cart.updated($items, $cart) | When an item is updated | 912 | | cart.removing($id, $cart) | When an item is being remove | 913 | | cart.removed($id, $cart) | When an item is removed | 914 | | cart.clearing(\$cart) | When a cart is attempted to be cleared | 915 | | cart.cleared(\$cart) | When a cart is cleared | 916 | 917 | **NOTE**: For different cart instance, dealing events is simple. For example you have created another cart instance which 918 | you have given an instance name of "wishlist". The Events will be something like: {$instanceName}.created($cart) 919 | 920 | So for you wishlist cart instance, events will look like this: 921 | 922 | - wishlist.created(\$cart) 923 | - wishlist.adding($items, $cart) 924 | - wishlist.added($items, $cart) and so on.. 925 | 926 | ## Format Response 927 | 928 | Now you can format all the responses. You can publish the config file from the package or use env vars to set the configuration. 929 | The options you have are: 930 | 931 | - format_numbers or env('SHOPPING_FORMAT_VALUES', false) => Activate or deactivate this feature. Default to false, 932 | - decimals or env('SHOPPING_DECIMALS', 0) => Number of decimals you want to show. Defaults to 0. 933 | - dec_point or env('SHOPPING_DEC_POINT', '.') => Decimal point type. Defaults to a '.'. 934 | - thousands_sep or env('SHOPPING_THOUSANDS_SEP', ',') => Thousands separator for value. Defaults to ','. 935 | 936 | ## Examples 937 | 938 | ```php 939 | 940 | // add items to cart 941 | Cart::add(array( 942 | array( 943 | 'id' => 456, 944 | 'name' => 'Sample Item 1', 945 | 'price' => 67.99, 946 | 'quantity' => 4, 947 | 'attributes' => array() 948 | ), 949 | array( 950 | 'id' => 568, 951 | 'name' => 'Sample Item 2', 952 | 'price' => 69.25, 953 | 'quantity' => 4, 954 | 'attributes' => array( 955 | 'size' => 'L', 956 | 'color' => 'blue' 957 | ) 958 | ), 959 | )); 960 | 961 | // then you can: 962 | $items = Cart::getContent(); 963 | 964 | foreach($items as $item) 965 | { 966 | $item->id; // the Id of the item 967 | $item->name; // the name 968 | $item->price; // the single price without conditions applied 969 | $item->getPriceSum(); // the subtotal without conditions applied 970 | $item->getPriceWithConditions(); // the single price with conditions applied 971 | $item->getPriceSumWithConditions(); // the subtotal with conditions applied 972 | $item->quantity; // the quantity 973 | $item->attributes; // the attributes 974 | 975 | // Note that attribute returns ItemAttributeCollection object that extends the native laravel collection 976 | // so you can do things like below: 977 | 978 | if( $item->attributes->has('size') ) 979 | { 980 | // item has attribute size 981 | } 982 | else 983 | { 984 | // item has no attribute size 985 | } 986 | } 987 | 988 | // or 989 | $items->each(function($item) 990 | { 991 | $item->id; // the Id of the item 992 | $item->name; // the name 993 | $item->price; // the single price without conditions applied 994 | $item->getPriceSum(); // the subtotal without conditions applied 995 | $item->getPriceWithConditions(); // the single price with conditions applied 996 | $item->getPriceSumWithConditions(); // the subtotal with conditions applied 997 | $item->quantity; // the quantity 998 | $item->attributes; // the attributes 999 | 1000 | if( $item->attributes->has('size') ) 1001 | { 1002 | // item has attribute size 1003 | } 1004 | else 1005 | { 1006 | // item has no attribute size 1007 | } 1008 | }); 1009 | 1010 | ``` 1011 | 1012 | ## Storage 1013 | 1014 | Using different storage for the carts items is pretty straight forward. The storage 1015 | class that is injected to the Cart's instance will only need methods. 1016 | 1017 | Example we will need a wishlist, and we want to store its key value pair in database instead 1018 | of the default session. 1019 | 1020 | To do this, we will need first a database table that will hold our cart data. 1021 | Let's create it by issuing `php artisan make:migration create_cart_storage_table` 1022 | 1023 | Example Code: 1024 | 1025 | ```php 1026 | use Illuminate\Support\Facades\Schema; 1027 | use Illuminate\Database\Schema\Blueprint; 1028 | use Illuminate\Database\Migrations\Migration; 1029 | 1030 | class CreateCartStorageTable extends Migration 1031 | { 1032 | /** 1033 | * Run the migrations. 1034 | * 1035 | * @return void 1036 | */ 1037 | public function up() 1038 | { 1039 | Schema::create('cart_storage', function (Blueprint $table) { 1040 | $table->string('id')->index(); 1041 | $table->longText('cart_data'); 1042 | $table->timestamps(); 1043 | 1044 | $table->primary('id'); 1045 | }); 1046 | } 1047 | 1048 | /** 1049 | * Reverse the migrations. 1050 | * 1051 | * @return void 1052 | */ 1053 | public function down() 1054 | { 1055 | Schema::dropIfExists('cart_storage'); 1056 | } 1057 | } 1058 | ``` 1059 | 1060 | Next, lets create an eloquent Model on this table so we can easily deal with the data. It is up to you where you want 1061 | to store this model. For this example, lets just assume to store it in our App namespace. 1062 | 1063 | Code: 1064 | 1065 | ```php 1066 | namespace App; 1067 | 1068 | use Illuminate\Database\Eloquent\Model; 1069 | 1070 | 1071 | class DatabaseStorageModel extends Model 1072 | { 1073 | protected $table = 'cart_storage'; 1074 | 1075 | /** 1076 | * The attributes that are mass assignable. 1077 | * 1078 | * @var array 1079 | */ 1080 | protected $fillable = [ 1081 | 'id', 'cart_data', 1082 | ]; 1083 | 1084 | public function setCartDataAttribute($value) 1085 | { 1086 | $this->attributes['cart_data'] = serialize($value); 1087 | } 1088 | 1089 | public function getCartDataAttribute($value) 1090 | { 1091 | return unserialize($value); 1092 | } 1093 | } 1094 | ``` 1095 | 1096 | Next, Create a new class for your storage to be injected to our cart instance: 1097 | 1098 | Eg. 1099 | 1100 | ```php 1101 | class DBStorage { 1102 | 1103 | public function has($key) 1104 | { 1105 | return DatabaseStorageModel::find($key); 1106 | } 1107 | 1108 | public function get($key) 1109 | { 1110 | if($this->has($key)) 1111 | { 1112 | return new CartCollection(DatabaseStorageModel::find($key)->cart_data); 1113 | } 1114 | else 1115 | { 1116 | return []; 1117 | } 1118 | } 1119 | 1120 | public function put($key, $value) 1121 | { 1122 | if($row = DatabaseStorageModel::find($key)) 1123 | { 1124 | // update 1125 | $row->cart_data = $value; 1126 | $row->save(); 1127 | } 1128 | else 1129 | { 1130 | DatabaseStorageModel::create([ 1131 | 'id' => $key, 1132 | 'cart_data' => $value 1133 | ]); 1134 | } 1135 | } 1136 | } 1137 | ``` 1138 | 1139 | For example you can also leverage Laravel's Caching (redis, memcached, file, dynamo, etc) using the example below. Example also includes cookie persistance, so that cart would be still available for 30 days. Sessions by default persists only 20 minutes. 1140 | 1141 | ```php 1142 | namespace App\Cart; 1143 | 1144 | use Carbon\Carbon; 1145 | use Cookie; 1146 | use Darryldecode\Cart\CartCollection; 1147 | 1148 | class CacheStorage 1149 | { 1150 | private $data = []; 1151 | private $cart_id; 1152 | 1153 | public function __construct() 1154 | { 1155 | $this->cart_id = \Cookie::get('cart'); 1156 | if ($this->cart_id) { 1157 | $this->data = \Cache::get('cart_' . $this->cart_id, []); 1158 | } else { 1159 | $this->cart_id = uniqid(); 1160 | } 1161 | } 1162 | 1163 | public function has($key) 1164 | { 1165 | return isset($this->data[$key]); 1166 | } 1167 | 1168 | public function get($key) 1169 | { 1170 | return new CartCollection($this->data[$key] ?? []); 1171 | } 1172 | 1173 | public function put($key, $value) 1174 | { 1175 | $this->data[$key] = $value; 1176 | \Cache::put('cart_' . $this->cart_id, $this->data, Carbon::now()->addDays(30)); 1177 | 1178 | if (!Cookie::hasQueued('cart')) { 1179 | Cookie::queue( 1180 | Cookie::make('cart', $this->cart_id, 60 * 24 * 30) 1181 | ); 1182 | } 1183 | } 1184 | } 1185 | ``` 1186 | 1187 | To make this the cart's default storage, let's update the cart's configuration file. 1188 | First, let us publish first the cart config file for us to enable to override it. 1189 | `php artisan vendor:publish --provider="Darryldecode\Cart\CartServiceProvider" --tag="config"` 1190 | 1191 | after running that command, there should be a new file on your config folder name `shopping_cart.php` 1192 | 1193 | Open this file and let's update the storage use. Find the key which says `'storage' => null,` 1194 | And update it to your newly created DBStorage Class, which on our example, 1195 | `'storage' => \App\DBStorage::class,` 1196 | 1197 | OR If you have multiple cart instance (example WishList), you can inject the custom database storage 1198 | to your cart instance by injecting it to the service provider of your wishlist cart, you replace the storage 1199 | to use your custom storage. See below: 1200 | 1201 | ```php 1202 | use Darryldecode\Cart\Cart; 1203 | use Illuminate\Support\ServiceProvider; 1204 | 1205 | class WishListProvider extends ServiceProvider 1206 | { 1207 | /** 1208 | * Bootstrap the application services. 1209 | * 1210 | * @return void 1211 | */ 1212 | public function boot() 1213 | { 1214 | // 1215 | } 1216 | /** 1217 | * Register the application services. 1218 | * 1219 | * @return void 1220 | */ 1221 | public function register() 1222 | { 1223 | $this->app->singleton('wishlist', function($app) 1224 | { 1225 | $storage = new DBStorage(); <-- Your new custom storage 1226 | $events = $app['events']; 1227 | $instanceName = 'cart_2'; 1228 | $session_key = '88uuiioo99888'; 1229 | return new Cart( 1230 | $storage, 1231 | $events, 1232 | $instanceName, 1233 | $session_key, 1234 | config('shopping_cart') 1235 | ); 1236 | }); 1237 | } 1238 | } 1239 | ``` 1240 | 1241 | Still feeling confuse on how to do custom database storage? Or maybe doing multiple cart instances? 1242 | See the demo repo to see the codes and how you can possibly do it and expand base on your needs or make it 1243 | as a guide & reference. See links below: 1244 | 1245 | [See Demo App Here](https://shoppingcart-demo.darrylfernandez.com/cart) 1246 | 1247 | OR 1248 | 1249 | [See Demo App Repo Here](https://github.com/darryldecode/laravelshoppingcart-demo) 1250 | 1251 | ## License 1252 | 1253 | The Laravel Shopping Cart is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 1254 | 1255 | ### Disclaimer 1256 | 1257 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR, OR ANY OF THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1258 | --------------------------------------------------------------------------------