├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── cart.php ├── database └── migrations │ └── 0000_00_00_000000_create_shoppingcart_table.php ├── phpunit.xml ├── src ├── CanBeBought.php ├── Cart.php ├── CartItem.php ├── CartItemOptions.php ├── Contracts │ └── Buyable.php ├── Exceptions │ ├── CartAlreadyStoredException.php │ ├── InvalidRowIDException.php │ └── UnknownModelException.php ├── Facades │ └── Cart.php └── ShoppingcartServiceProvider.php └── tests ├── CartAssertions.php ├── CartItemTest.php ├── CartTest.php └── Fixtures ├── BuyableProduct.php └── ProductModel.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | 7 | before_script: 8 | - composer self-update 9 | - composer install --prefer-source --no-interaction 10 | 11 | script: vendor/bin/phpunit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Rob Gloudemans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LaravelShoppingcart 2 | [![Build Status](https://travis-ci.org/Crinsane/LaravelShoppingcart.png?branch=master)](https://travis-ci.org/Crinsane/LaravelShoppingcart) 3 | [![Total Downloads](https://poser.pugx.org/gloudemans/shoppingcart/downloads.png)](https://packagist.org/packages/gloudemans/shoppingcart) 4 | [![Latest Stable Version](https://poser.pugx.org/gloudemans/shoppingcart/v/stable)](https://packagist.org/packages/gloudemans/shoppingcart) 5 | [![Latest Unstable Version](https://poser.pugx.org/gloudemans/shoppingcart/v/unstable)](https://packagist.org/packages/gloudemans/shoppingcart) 6 | [![License](https://poser.pugx.org/gloudemans/shoppingcart/license)](https://packagist.org/packages/gloudemans/shoppingcart) 7 | 8 | A simple shoppingcart implementation for Laravel. 9 | 10 | ## Installation 11 | 12 | Install the package through [Composer](http://getcomposer.org/). 13 | 14 | Run the Composer require command from the Terminal: 15 | 16 | composer require gloudemans/shoppingcart 17 | 18 | If you're using Laravel 5.5, this is all there is to do. 19 | 20 | Should you still be on version 5.4 of Laravel, the final steps for you are to add the service provider of the package and alias the package. To do this open your `config/app.php` file. 21 | 22 | Add a new line to the `providers` array: 23 | 24 | Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class 25 | 26 | And optionally add a new line to the `aliases` array: 27 | 28 | 'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class, 29 | 30 | Now you're ready to start using the shoppingcart in your application. 31 | 32 | **As of version 2 of this package it's possibly to use dependency injection to inject an instance of the Cart class into your controller or other class** 33 | 34 | ## Overview 35 | Look at one of the following topics to learn more about LaravelShoppingcart 36 | 37 | * [Usage](#usage) 38 | * [Collections](#collections) 39 | * [Instances](#instances) 40 | * [Models](#models) 41 | * [Database](#database) 42 | * [Exceptions](#exceptions) 43 | * [Events](#events) 44 | * [Example](#example) 45 | 46 | ## Usage 47 | 48 | The shoppingcart gives you the following methods to use: 49 | 50 | ### Cart::add() 51 | 52 | Adding an item to the cart is really simple, you just use the `add()` method, which accepts a variety of parameters. 53 | 54 | In its most basic form you can specify the id, name, quantity, price of the product you'd like to add to the cart. 55 | 56 | ```php 57 | Cart::add('293ad', 'Product 1', 1, 9.99); 58 | ``` 59 | 60 | As an optional fifth parameter you can pass it options, so you can add multiple items with the same id, but with (for instance) a different size. 61 | 62 | ```php 63 | Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large']); 64 | ``` 65 | 66 | **The `add()` method will return an CartItem instance of the item you just added to the cart.** 67 | 68 | Maybe you prefer to add the item using an array? As long as the array contains the required keys, you can pass it to the method. The options key is optional. 69 | 70 | ```php 71 | Cart::add(['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 9.99, 'options' => ['size' => 'large']]); 72 | ``` 73 | 74 | New in version 2 of the package is the possibility to work with the `Buyable` interface. The way this works is that you have a model implement the `Buyable` interface, which will make you implement a few methods so the package knows how to get the id, name and price from your model. 75 | This way you can just pass the `add()` method a model and the quantity and it will automatically add it to the cart. 76 | 77 | **As an added bonus it will automatically associate the model with the CartItem** 78 | 79 | ```php 80 | Cart::add($product, 1, ['size' => 'large']); 81 | ``` 82 | As an optional third parameter you can add options. 83 | ```php 84 | Cart::add($product, 1, ['size' => 'large']); 85 | ``` 86 | 87 | Finally, you can also add multipe items to the cart at once. 88 | You can just pass the `add()` method an array of arrays, or an array of Buyables and they will be added to the cart. 89 | 90 | **When adding multiple items to the cart, the `add()` method will return an array of CartItems.** 91 | 92 | ```php 93 | Cart::add([ 94 | ['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 10.00], 95 | ['id' => '4832k', 'name' => 'Product 2', 'qty' => 1, 'price' => 10.00, 'options' => ['size' => 'large']] 96 | ]); 97 | 98 | Cart::add([$product1, $product2]); 99 | 100 | ``` 101 | 102 | ### Cart::update() 103 | 104 | To update an item in the cart, you'll first need the rowId of the item. 105 | Next you can use the `update()` method to update it. 106 | 107 | If you simply want to update the quantity, you'll pass the update method the rowId and the new quantity: 108 | 109 | ```php 110 | $rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'; 111 | 112 | Cart::update($rowId, 2); // Will update the quantity 113 | ``` 114 | 115 | If you want to update more attributes of the item, you can either pass the update method an array or a `Buyable` as the second parameter. This way you can update all information of the item with the given rowId. 116 | 117 | ```php 118 | Cart::update($rowId, ['name' => 'Product 1']); // Will update the name 119 | 120 | Cart::update($rowId, $product); // Will update the id, name and price 121 | 122 | ``` 123 | 124 | ### Cart::remove() 125 | 126 | To remove an item for the cart, you'll again need the rowId. This rowId you simply pass to the `remove()` method and it will remove the item from the cart. 127 | 128 | ```php 129 | $rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'; 130 | 131 | Cart::remove($rowId); 132 | ``` 133 | 134 | ### Cart::get() 135 | 136 | If you want to get an item from the cart using its rowId, you can simply call the `get()` method on the cart and pass it the rowId. 137 | 138 | ```php 139 | $rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'; 140 | 141 | Cart::get($rowId); 142 | ``` 143 | 144 | ### Cart::content() 145 | 146 | Of course you also want to get the carts content. This is where you'll use the `content` method. This method will return a Collection of CartItems which you can iterate over and show the content to your customers. 147 | 148 | ```php 149 | Cart::content(); 150 | ``` 151 | 152 | This method will return the content of the current cart instance, if you want the content of another instance, simply chain the calls. 153 | 154 | ```php 155 | Cart::instance('wishlist')->content(); 156 | ``` 157 | 158 | ### Cart::destroy() 159 | 160 | If you want to completely remove the content of a cart, you can call the destroy method on the cart. This will remove all CartItems from the cart for the current cart instance. 161 | 162 | ```php 163 | Cart::destroy(); 164 | ``` 165 | 166 | ### Cart::total() 167 | 168 | The `total()` method can be used to get the calculated total of all items in the cart, given there price and quantity. 169 | 170 | ```php 171 | Cart::total(); 172 | ``` 173 | 174 | The method will automatically format the result, which you can tweak using the three optional parameters 175 | 176 | ```php 177 | Cart::total($decimals, $decimalSeperator, $thousandSeperator); 178 | ``` 179 | 180 | You can set the default number format in the config file. 181 | 182 | **If you're not using the Facade, but use dependency injection in your (for instance) Controller, you can also simply get the total property `$cart->total`** 183 | 184 | ### Cart::tax() 185 | 186 | The `tax()` method can be used to get the calculated amount of tax for all items in the cart, given there price and quantity. 187 | 188 | ```php 189 | Cart::tax(); 190 | ``` 191 | 192 | The method will automatically format the result, which you can tweak using the three optional parameters 193 | 194 | ```php 195 | Cart::tax($decimals, $decimalSeperator, $thousandSeperator); 196 | ``` 197 | 198 | You can set the default number format in the config file. 199 | 200 | **If you're not using the Facade, but use dependency injection in your (for instance) Controller, you can also simply get the tax property `$cart->tax`** 201 | 202 | ### Cart::subtotal() 203 | 204 | The `subtotal()` method can be used to get the total of all items in the cart, minus the total amount of tax. 205 | 206 | ```php 207 | Cart::subtotal(); 208 | ``` 209 | 210 | The method will automatically format the result, which you can tweak using the three optional parameters 211 | 212 | ```php 213 | Cart::subtotal($decimals, $decimalSeperator, $thousandSeperator); 214 | ``` 215 | 216 | You can set the default number format in the config file. 217 | 218 | **If you're not using the Facade, but use dependency injection in your (for instance) Controller, you can also simply get the subtotal property `$cart->subtotal`** 219 | 220 | ### Cart::count() 221 | 222 | If you want to know how many items there are in your cart, you can use the `count()` method. This method will return the total number of items in the cart. So if you've added 2 books and 1 shirt, it will return 3 items. 223 | 224 | ```php 225 | Cart::count(); 226 | ``` 227 | 228 | ### Cart::search() 229 | 230 | To find an item in the cart, you can use the `search()` method. 231 | 232 | **This method was changed on version 2** 233 | 234 | Behind the scenes, the method simply uses the filter method of the Laravel Collection class. This means you must pass it a Closure in which you'll specify you search terms. 235 | 236 | If you for instance want to find all items with an id of 1: 237 | 238 | ```php 239 | $cart->search(function ($cartItem, $rowId) { 240 | return $cartItem->id === 1; 241 | }); 242 | ``` 243 | 244 | As you can see the Closure will receive two parameters. The first is the CartItem to perform the check against. The second parameter is the rowId of this CartItem. 245 | 246 | **The method will return a Collection containing all CartItems that where found** 247 | 248 | This way of searching gives you total control over the search process and gives you the ability to create very precise and specific searches. 249 | 250 | ## Collections 251 | 252 | On multiple instances the Cart will return to you a Collection. This is just a simple Laravel Collection, so all methods you can call on a Laravel Collection are also available on the result. 253 | 254 | As an example, you can quicky get the number of unique products in a cart: 255 | 256 | ```php 257 | Cart::content()->count(); 258 | ``` 259 | 260 | Or you can group the content by the id of the products: 261 | 262 | ```php 263 | Cart::content()->groupBy('id'); 264 | ``` 265 | 266 | ## Instances 267 | 268 | The packages supports multiple instances of the cart. The way this works is like this: 269 | 270 | You can set the current instance of the cart by calling `Cart::instance('newInstance')`. From this moment, the active instance of the cart will be `newInstance`, so when you add, remove or get the content of the cart, you're work with the `newInstance` instance of the cart. 271 | If you want to switch instances, you just call `Cart::instance('otherInstance')` again, and you're working with the `otherInstance` again. 272 | 273 | So a little example: 274 | 275 | ```php 276 | Cart::instance('shopping')->add('192ao12', 'Product 1', 1, 9.99); 277 | 278 | // Get the content of the 'shopping' cart 279 | Cart::content(); 280 | 281 | Cart::instance('wishlist')->add('sdjk922', 'Product 2', 1, 19.95, ['size' => 'medium']); 282 | 283 | // Get the content of the 'wishlist' cart 284 | Cart::content(); 285 | 286 | // If you want to get the content of the 'shopping' cart again 287 | Cart::instance('shopping')->content(); 288 | 289 | // And the count of the 'wishlist' cart again 290 | Cart::instance('wishlist')->count(); 291 | ``` 292 | 293 | **N.B. Keep in mind that the cart stays in the last set instance for as long as you don't set a different one during script execution.** 294 | 295 | **N.B.2 The default cart instance is called `default`, so when you're not using instances,`Cart::content();` is the same as `Cart::instance('default')->content()`.** 296 | 297 | ## Models 298 | 299 | Because it can be very convenient to be able to directly access a model from a CartItem is it possible to associate a model with the items in the cart. 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. 300 | 301 | That way you can access your model right from the `CartItem`! 302 | 303 | The model can be accessed via the `model` property on the CartItem. 304 | 305 | **If your model implements the `Buyable` interface and you used your model to add the item to the cart, it will associate automatically.** 306 | 307 | Here is an example: 308 | 309 | ```php 310 | 311 | // First we'll add the item to the cart. 312 | $cartItem = Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large']); 313 | 314 | // Next we associate a model with the item. 315 | Cart::associate($cartItem->rowId, 'Product'); 316 | 317 | // Or even easier, call the associate method on the CartItem! 318 | $cartItem->associate('Product'); 319 | 320 | // You can even make it a one-liner 321 | Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large'])->associate('Product'); 322 | 323 | // Now, when iterating over the content of the cart, you can access the model. 324 | foreach(Cart::content() as $row) { 325 | echo 'You have ' . $row->qty . ' items of ' . $row->model->name . ' with description: "' . $row->model->description . '" in your cart.'; 326 | } 327 | ``` 328 | ## Database 329 | 330 | - [Config](#configuration) 331 | - [Storing the cart](#save-cart-to-database) 332 | - [Restoring the cart](#retrieve-cart-from-database) 333 | 334 | ### Configuration 335 | To save cart into the database so you can retrieve it later, the package needs to know which database connection to use and what the name of the table is. 336 | By default the package will use the default database connection and use a table named `shoppingcart`. 337 | If you want to change these options, you'll have to publish the `config` file. 338 | 339 | php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config" 340 | 341 | This will give you a `cart.php` config file in which you can make the changes. 342 | 343 | To make your life easy, the package also includes a ready to use `migration` which you can publish by running: 344 | 345 | php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="migrations" 346 | 347 | This will place a `shoppingcart` table's migration file into `database/migrations` directory. Now all you have to do is run `php artisan migrate` to migrate your database. 348 | 349 | ### Storing the cart 350 | To store your cart instance into the database, you have to call the `store($identifier) ` method. Where `$identifier` is a random key, for instance the id or username of the user. 351 | 352 | Cart::store('username'); 353 | 354 | // To store a cart instance named 'wishlist' 355 | Cart::instance('wishlist')->store('username'); 356 | 357 | ### Restoring the cart 358 | If you want to retrieve the cart from the database and restore it, all you have to do is call the `restore($identifier)` where `$identifier` is the key you specified for the `store` method. 359 | 360 | Cart::restore('username'); 361 | 362 | // To restore a cart instance named 'wishlist' 363 | Cart::instance('wishlist')->restore('username'); 364 | 365 | ## Exceptions 366 | 367 | The Cart package will throw exceptions if something goes wrong. This way it's easier to debug your code using the Cart package or to handle the error based on the type of exceptions. The Cart packages can throw the following exceptions: 368 | 369 | | Exception | Reason | 370 | | ---------------------------- | ---------------------------------------------------------------------------------- | 371 | | *CartAlreadyStoredException* | When trying to store a cart that was already stored using the specified identifier | 372 | | *InvalidRowIDException* | When the rowId that got passed doesn't exists in the current cart instance | 373 | | *UnknownModelException* | When you try to associate an none existing model to a CartItem. | 374 | 375 | ## Events 376 | 377 | The cart also has events build in. There are five events available for you to listen for. 378 | 379 | | Event | Fired | Parameter | 380 | | ------------- | ---------------------------------------- | -------------------------------- | 381 | | cart.added | When an item was added to the cart. | The `CartItem` that was added. | 382 | | cart.updated | When an item in the cart was updated. | The `CartItem` that was updated. | 383 | | cart.removed | When an item is removed from the cart. | The `CartItem` that was removed. | 384 | | cart.stored | When the content of a cart was stored. | - | 385 | | cart.restored | When the content of a cart was restored. | - | 386 | 387 | ## Example 388 | 389 | Below is a little example of how to list the cart content in a table: 390 | 391 | ```php 392 | 393 | // Add some items in your Controller. 394 | Cart::add('192ao12', 'Product 1', 1, 9.99); 395 | Cart::add('1239ad0', 'Product 2', 2, 5.95, ['size' => 'large']); 396 | 397 | // Display the content in a View. 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 |
ProductQtyPriceSubtotal
414 |

name; ?>

415 |

options->has('size') ? $row->options->size : ''); ?>

416 |
$price; ?>$total; ?>
 Subtotal
 Tax
 Total
444 | ``` 445 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gloudemans/shoppingcart", 3 | "description": "Laravel Shoppingcart", 4 | "keywords": ["laravel", "shoppingcart"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Rob Gloudemans", 9 | "email": "info@robgloudemans.nl" 10 | } 11 | ], 12 | "require": { 13 | "illuminate/support": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.*|| 5.6.* || 5.7.*", 14 | "illuminate/session": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.*|| 5.6.* || 5.7.*", 15 | "illuminate/events": "5.1.* || 5.2.* || 5.3.* || 5.4.* || 5.5.*|| 5.6.* || 5.7.*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~5.0 || ~6.0 || ~7.0", 19 | "mockery/mockery": "~0.9.0", 20 | "orchestra/testbench": "~3.1" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Gloudemans\\Shoppingcart\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Gloudemans\\Tests\\Shoppingcart\\": "tests/" 30 | } 31 | }, 32 | "suggest": { 33 | "gloudemans/notify": "Simple flash notifications for Laravel" 34 | }, 35 | "minimum-stability": "stable", 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "Gloudemans\\Shoppingcart\\ShoppingcartServiceProvider" 40 | ], 41 | "aliases": { 42 | "Cart": "Gloudemans\\Shoppingcart\\Facades\\Cart" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/cart.php: -------------------------------------------------------------------------------- 1 | 21, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Shoppingcart database settings 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Here you can set the connection that the shoppingcart should use when 23 | | storing and restoring a cart. 24 | | 25 | */ 26 | 27 | 'database' => [ 28 | 29 | 'connection' => null, 30 | 31 | 'table' => 'shoppingcart', 32 | 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Destroy the cart on user logout 38 | |-------------------------------------------------------------------------- 39 | | 40 | | When this option is set to 'true' the cart will automatically 41 | | destroy all cart instances when the user logs out. 42 | | 43 | */ 44 | 45 | 'destroy_on_logout' => false, 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Default number format 50 | |-------------------------------------------------------------------------- 51 | | 52 | | This defaults will be used for the formated numbers if you don't 53 | | set them in the method call. 54 | | 55 | */ 56 | 57 | 'format' => [ 58 | 59 | 'decimals' => 2, 60 | 61 | 'decimal_point' => '.', 62 | 63 | 'thousand_seperator' => ',' 64 | 65 | ], 66 | 67 | ]; -------------------------------------------------------------------------------- /database/migrations/0000_00_00_000000_create_shoppingcart_table.php: -------------------------------------------------------------------------------- 1 | string('identifier'); 16 | $table->string('instance'); 17 | $table->longText('content'); 18 | $table->nullableTimestamps(); 19 | 20 | $table->primary(['identifier', 'instance']); 21 | }); 22 | } 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down() 27 | { 28 | Schema::drop(config('cart.database.table')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/CanBeBought.php: -------------------------------------------------------------------------------- 1 | getKey() : $this->id; 16 | } 17 | 18 | /** 19 | * Get the description or title of the Buyable item. 20 | * 21 | * @return string 22 | */ 23 | public function getBuyableDescription($options = null) 24 | { 25 | if(property_exists($this, 'name')) return $this->name; 26 | if(property_exists($this, 'title')) return $this->title; 27 | if(property_exists($this, 'description')) return $this->description; 28 | 29 | return null; 30 | } 31 | 32 | /** 33 | * Get the price of the Buyable item. 34 | * 35 | * @return float 36 | */ 37 | public function getBuyablePrice($options = null) 38 | { 39 | if(property_exists($this, 'price')) return $this->price; 40 | 41 | return null; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Cart.php: -------------------------------------------------------------------------------- 1 | session = $session; 49 | $this->events = $events; 50 | 51 | $this->instance(self::DEFAULT_INSTANCE); 52 | } 53 | 54 | /** 55 | * Set the current cart instance. 56 | * 57 | * @param string|null $instance 58 | * @return \Gloudemans\Shoppingcart\Cart 59 | */ 60 | public function instance($instance = null) 61 | { 62 | $instance = $instance ?: self::DEFAULT_INSTANCE; 63 | 64 | $this->instance = sprintf('%s.%s', 'cart', $instance); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Get the current cart instance. 71 | * 72 | * @return string 73 | */ 74 | public function currentInstance() 75 | { 76 | return str_replace('cart.', '', $this->instance); 77 | } 78 | 79 | /** 80 | * Add an item to the cart. 81 | * 82 | * @param mixed $id 83 | * @param mixed $name 84 | * @param int|float $qty 85 | * @param float $price 86 | * @param array $options 87 | * @return \Gloudemans\Shoppingcart\CartItem 88 | */ 89 | public function add($id, $name = null, $qty = null, $price = null, array $options = []) 90 | { 91 | if ($this->isMulti($id)) { 92 | return array_map(function ($item) { 93 | return $this->add($item); 94 | }, $id); 95 | } 96 | 97 | $cartItem = $this->createCartItem($id, $name, $qty, $price, $options); 98 | 99 | $content = $this->getContent(); 100 | 101 | if ($content->has($cartItem->rowId)) { 102 | $cartItem->qty += $content->get($cartItem->rowId)->qty; 103 | } 104 | 105 | $content->put($cartItem->rowId, $cartItem); 106 | 107 | $this->events->fire('cart.added', $cartItem); 108 | 109 | $this->session->put($this->instance, $content); 110 | 111 | return $cartItem; 112 | } 113 | 114 | /** 115 | * Update the cart item with the given rowId. 116 | * 117 | * @param string $rowId 118 | * @param mixed $qty 119 | * @return \Gloudemans\Shoppingcart\CartItem 120 | */ 121 | public function update($rowId, $qty) 122 | { 123 | $cartItem = $this->get($rowId); 124 | 125 | if ($qty instanceof Buyable) { 126 | $cartItem->updateFromBuyable($qty); 127 | } elseif (is_array($qty)) { 128 | $cartItem->updateFromArray($qty); 129 | } else { 130 | $cartItem->qty = $qty; 131 | } 132 | 133 | $content = $this->getContent(); 134 | 135 | if ($rowId !== $cartItem->rowId) { 136 | $content->pull($rowId); 137 | 138 | if ($content->has($cartItem->rowId)) { 139 | $existingCartItem = $this->get($cartItem->rowId); 140 | $cartItem->setQuantity($existingCartItem->qty + $cartItem->qty); 141 | } 142 | } 143 | 144 | if ($cartItem->qty <= 0) { 145 | $this->remove($cartItem->rowId); 146 | return; 147 | } else { 148 | $content->put($cartItem->rowId, $cartItem); 149 | } 150 | 151 | $this->events->fire('cart.updated', $cartItem); 152 | 153 | $this->session->put($this->instance, $content); 154 | 155 | return $cartItem; 156 | } 157 | 158 | /** 159 | * Remove the cart item with the given rowId from the cart. 160 | * 161 | * @param string $rowId 162 | * @return void 163 | */ 164 | public function remove($rowId) 165 | { 166 | $cartItem = $this->get($rowId); 167 | 168 | $content = $this->getContent(); 169 | 170 | $content->pull($cartItem->rowId); 171 | 172 | $this->events->fire('cart.removed', $cartItem); 173 | 174 | $this->session->put($this->instance, $content); 175 | } 176 | 177 | /** 178 | * Get a cart item from the cart by its rowId. 179 | * 180 | * @param string $rowId 181 | * @return \Gloudemans\Shoppingcart\CartItem 182 | */ 183 | public function get($rowId) 184 | { 185 | $content = $this->getContent(); 186 | 187 | if ( ! $content->has($rowId)) 188 | throw new InvalidRowIDException("The cart does not contain rowId {$rowId}."); 189 | 190 | return $content->get($rowId); 191 | } 192 | 193 | /** 194 | * Destroy the current cart instance. 195 | * 196 | * @return void 197 | */ 198 | public function destroy() 199 | { 200 | $this->session->remove($this->instance); 201 | } 202 | 203 | /** 204 | * Get the content of the cart. 205 | * 206 | * @return \Illuminate\Support\Collection 207 | */ 208 | public function content() 209 | { 210 | if (is_null($this->session->get($this->instance))) { 211 | return new Collection([]); 212 | } 213 | 214 | return $this->session->get($this->instance); 215 | } 216 | 217 | /** 218 | * Get the number of items in the cart. 219 | * 220 | * @return int|float 221 | */ 222 | public function count() 223 | { 224 | $content = $this->getContent(); 225 | 226 | return $content->sum('qty'); 227 | } 228 | 229 | /** 230 | * Get the total price of the items in the cart. 231 | * 232 | * @param int $decimals 233 | * @param string $decimalPoint 234 | * @param string $thousandSeperator 235 | * @return string 236 | */ 237 | public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null) 238 | { 239 | $content = $this->getContent(); 240 | 241 | $total = $content->reduce(function ($total, CartItem $cartItem) { 242 | return $total + ($cartItem->qty * $cartItem->priceTax); 243 | }, 0); 244 | 245 | return $this->numberFormat($total, $decimals, $decimalPoint, $thousandSeperator); 246 | } 247 | 248 | /** 249 | * Get the total tax of the items in the cart. 250 | * 251 | * @param int $decimals 252 | * @param string $decimalPoint 253 | * @param string $thousandSeperator 254 | * @return float 255 | */ 256 | public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null) 257 | { 258 | $content = $this->getContent(); 259 | 260 | $tax = $content->reduce(function ($tax, CartItem $cartItem) { 261 | return $tax + ($cartItem->qty * $cartItem->tax); 262 | }, 0); 263 | 264 | return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator); 265 | } 266 | 267 | /** 268 | * Get the subtotal (total - tax) of the items in the cart. 269 | * 270 | * @param int $decimals 271 | * @param string $decimalPoint 272 | * @param string $thousandSeperator 273 | * @return float 274 | */ 275 | public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null) 276 | { 277 | $content = $this->getContent(); 278 | 279 | $subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) { 280 | return $subTotal + ($cartItem->qty * $cartItem->price); 281 | }, 0); 282 | 283 | return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator); 284 | } 285 | 286 | /** 287 | * Search the cart content for a cart item matching the given search closure. 288 | * 289 | * @param \Closure $search 290 | * @return \Illuminate\Support\Collection 291 | */ 292 | public function search(Closure $search) 293 | { 294 | $content = $this->getContent(); 295 | 296 | return $content->filter($search); 297 | } 298 | 299 | /** 300 | * Associate the cart item with the given rowId with the given model. 301 | * 302 | * @param string $rowId 303 | * @param mixed $model 304 | * @return void 305 | */ 306 | public function associate($rowId, $model) 307 | { 308 | if(is_string($model) && ! class_exists($model)) { 309 | throw new UnknownModelException("The supplied model {$model} does not exist."); 310 | } 311 | 312 | $cartItem = $this->get($rowId); 313 | 314 | $cartItem->associate($model); 315 | 316 | $content = $this->getContent(); 317 | 318 | $content->put($cartItem->rowId, $cartItem); 319 | 320 | $this->session->put($this->instance, $content); 321 | } 322 | 323 | /** 324 | * Set the tax rate for the cart item with the given rowId. 325 | * 326 | * @param string $rowId 327 | * @param int|float $taxRate 328 | * @return void 329 | */ 330 | public function setTax($rowId, $taxRate) 331 | { 332 | $cartItem = $this->get($rowId); 333 | 334 | $cartItem->setTaxRate($taxRate); 335 | 336 | $content = $this->getContent(); 337 | 338 | $content->put($cartItem->rowId, $cartItem); 339 | 340 | $this->session->put($this->instance, $content); 341 | } 342 | 343 | /** 344 | * Store an the current instance of the cart. 345 | * 346 | * @param mixed $identifier 347 | * @return void 348 | */ 349 | public function store($identifier) 350 | { 351 | $content = $this->getContent(); 352 | 353 | if ($this->storedCartWithIdentifierExists($identifier)) { 354 | throw new CartAlreadyStoredException("A cart with identifier {$identifier} was already stored."); 355 | } 356 | 357 | $this->getConnection()->table($this->getTableName())->insert([ 358 | 'identifier' => $identifier, 359 | 'instance' => $this->currentInstance(), 360 | 'content' => serialize($content) 361 | ]); 362 | 363 | $this->events->fire('cart.stored'); 364 | } 365 | 366 | /** 367 | * Restore the cart with the given identifier. 368 | * 369 | * @param mixed $identifier 370 | * @return void 371 | */ 372 | public function restore($identifier) 373 | { 374 | if( ! $this->storedCartWithIdentifierExists($identifier)) { 375 | return; 376 | } 377 | 378 | $stored = $this->getConnection()->table($this->getTableName()) 379 | ->where('identifier', $identifier)->first(); 380 | 381 | $storedContent = unserialize($stored->content); 382 | 383 | $currentInstance = $this->currentInstance(); 384 | 385 | $this->instance($stored->instance); 386 | 387 | $content = $this->getContent(); 388 | 389 | foreach ($storedContent as $cartItem) { 390 | $content->put($cartItem->rowId, $cartItem); 391 | } 392 | 393 | $this->events->fire('cart.restored'); 394 | 395 | $this->session->put($this->instance, $content); 396 | 397 | $this->instance($currentInstance); 398 | 399 | $this->getConnection()->table($this->getTableName()) 400 | ->where('identifier', $identifier)->delete(); 401 | } 402 | 403 | /** 404 | * Magic method to make accessing the total, tax and subtotal properties possible. 405 | * 406 | * @param string $attribute 407 | * @return float|null 408 | */ 409 | public function __get($attribute) 410 | { 411 | if($attribute === 'total') { 412 | return $this->total(); 413 | } 414 | 415 | if($attribute === 'tax') { 416 | return $this->tax(); 417 | } 418 | 419 | if($attribute === 'subtotal') { 420 | return $this->subtotal(); 421 | } 422 | 423 | return null; 424 | } 425 | 426 | /** 427 | * Get the carts content, if there is no cart content set yet, return a new empty Collection 428 | * 429 | * @return \Illuminate\Support\Collection 430 | */ 431 | protected function getContent() 432 | { 433 | $content = $this->session->has($this->instance) 434 | ? $this->session->get($this->instance) 435 | : new Collection; 436 | 437 | return $content; 438 | } 439 | 440 | /** 441 | * Create a new CartItem from the supplied attributes. 442 | * 443 | * @param mixed $id 444 | * @param mixed $name 445 | * @param int|float $qty 446 | * @param float $price 447 | * @param array $options 448 | * @return \Gloudemans\Shoppingcart\CartItem 449 | */ 450 | private function createCartItem($id, $name, $qty, $price, array $options) 451 | { 452 | if ($id instanceof Buyable) { 453 | $cartItem = CartItem::fromBuyable($id, $qty ?: []); 454 | $cartItem->setQuantity($name ?: 1); 455 | $cartItem->associate($id); 456 | } elseif (is_array($id)) { 457 | $cartItem = CartItem::fromArray($id); 458 | $cartItem->setQuantity($id['qty']); 459 | } else { 460 | $cartItem = CartItem::fromAttributes($id, $name, $price, $options); 461 | $cartItem->setQuantity($qty); 462 | } 463 | 464 | $cartItem->setTaxRate(config('cart.tax')); 465 | 466 | return $cartItem; 467 | } 468 | 469 | /** 470 | * Check if the item is a multidimensional array or an array of Buyables. 471 | * 472 | * @param mixed $item 473 | * @return bool 474 | */ 475 | private function isMulti($item) 476 | { 477 | if ( ! is_array($item)) return false; 478 | 479 | return is_array(head($item)) || head($item) instanceof Buyable; 480 | } 481 | 482 | /** 483 | * @param $identifier 484 | * @return bool 485 | */ 486 | private function storedCartWithIdentifierExists($identifier) 487 | { 488 | return $this->getConnection()->table($this->getTableName())->where('identifier', $identifier)->exists(); 489 | } 490 | 491 | /** 492 | * Get the database connection. 493 | * 494 | * @return \Illuminate\Database\Connection 495 | */ 496 | private function getConnection() 497 | { 498 | $connectionName = $this->getConnectionName(); 499 | 500 | return app(DatabaseManager::class)->connection($connectionName); 501 | } 502 | 503 | /** 504 | * Get the database table name. 505 | * 506 | * @return string 507 | */ 508 | private function getTableName() 509 | { 510 | return config('cart.database.table', 'shoppingcart'); 511 | } 512 | 513 | /** 514 | * Get the database connection name. 515 | * 516 | * @return string 517 | */ 518 | private function getConnectionName() 519 | { 520 | $connection = config('cart.database.connection'); 521 | 522 | return is_null($connection) ? config('database.default') : $connection; 523 | } 524 | 525 | /** 526 | * Get the Formated number 527 | * 528 | * @param $value 529 | * @param $decimals 530 | * @param $decimalPoint 531 | * @param $thousandSeperator 532 | * @return string 533 | */ 534 | private function numberFormat($value, $decimals, $decimalPoint, $thousandSeperator) 535 | { 536 | if(is_null($decimals)){ 537 | $decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals'); 538 | } 539 | if(is_null($decimalPoint)){ 540 | $decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point'); 541 | } 542 | if(is_null($thousandSeperator)){ 543 | $thousandSeperator = is_null(config('cart.format.thousand_seperator')) ? ',' : config('cart.format.thousand_seperator'); 544 | } 545 | 546 | return number_format($value, $decimals, $decimalPoint, $thousandSeperator); 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/CartItem.php: -------------------------------------------------------------------------------- 1 | id = $id; 88 | $this->name = $name; 89 | $this->price = floatval($price); 90 | $this->options = new CartItemOptions($options); 91 | $this->rowId = $this->generateRowId($id, $options); 92 | } 93 | 94 | /** 95 | * Returns the formatted price without TAX. 96 | * 97 | * @param int $decimals 98 | * @param string $decimalPoint 99 | * @param string $thousandSeperator 100 | * @return string 101 | */ 102 | public function price($decimals = null, $decimalPoint = null, $thousandSeperator = null) 103 | { 104 | return $this->numberFormat($this->price, $decimals, $decimalPoint, $thousandSeperator); 105 | } 106 | 107 | /** 108 | * Returns the formatted price with TAX. 109 | * 110 | * @param int $decimals 111 | * @param string $decimalPoint 112 | * @param string $thousandSeperator 113 | * @return string 114 | */ 115 | public function priceTax($decimals = null, $decimalPoint = null, $thousandSeperator = null) 116 | { 117 | return $this->numberFormat($this->priceTax, $decimals, $decimalPoint, $thousandSeperator); 118 | } 119 | 120 | /** 121 | * Returns the formatted subtotal. 122 | * Subtotal is price for whole CartItem without TAX 123 | * 124 | * @param int $decimals 125 | * @param string $decimalPoint 126 | * @param string $thousandSeperator 127 | * @return string 128 | */ 129 | public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null) 130 | { 131 | return $this->numberFormat($this->subtotal, $decimals, $decimalPoint, $thousandSeperator); 132 | } 133 | 134 | /** 135 | * Returns the formatted total. 136 | * Total is price for whole CartItem with TAX 137 | * 138 | * @param int $decimals 139 | * @param string $decimalPoint 140 | * @param string $thousandSeperator 141 | * @return string 142 | */ 143 | public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null) 144 | { 145 | return $this->numberFormat($this->total, $decimals, $decimalPoint, $thousandSeperator); 146 | } 147 | 148 | /** 149 | * Returns the formatted tax. 150 | * 151 | * @param int $decimals 152 | * @param string $decimalPoint 153 | * @param string $thousandSeperator 154 | * @return string 155 | */ 156 | public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null) 157 | { 158 | return $this->numberFormat($this->tax, $decimals, $decimalPoint, $thousandSeperator); 159 | } 160 | 161 | /** 162 | * Returns the formatted tax. 163 | * 164 | * @param int $decimals 165 | * @param string $decimalPoint 166 | * @param string $thousandSeperator 167 | * @return string 168 | */ 169 | public function taxTotal($decimals = null, $decimalPoint = null, $thousandSeperator = null) 170 | { 171 | return $this->numberFormat($this->taxTotal, $decimals, $decimalPoint, $thousandSeperator); 172 | } 173 | 174 | /** 175 | * Set the quantity for this cart item. 176 | * 177 | * @param int|float $qty 178 | */ 179 | public function setQuantity($qty) 180 | { 181 | if(empty($qty) || ! is_numeric($qty)) 182 | throw new \InvalidArgumentException('Please supply a valid quantity.'); 183 | 184 | $this->qty = $qty; 185 | } 186 | 187 | /** 188 | * Update the cart item from a Buyable. 189 | * 190 | * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item 191 | * @return void 192 | */ 193 | public function updateFromBuyable(Buyable $item) 194 | { 195 | $this->id = $item->getBuyableIdentifier($this->options); 196 | $this->name = $item->getBuyableDescription($this->options); 197 | $this->price = $item->getBuyablePrice($this->options); 198 | $this->priceTax = $this->price + $this->tax; 199 | } 200 | 201 | /** 202 | * Update the cart item from an array. 203 | * 204 | * @param array $attributes 205 | * @return void 206 | */ 207 | public function updateFromArray(array $attributes) 208 | { 209 | $this->id = array_get($attributes, 'id', $this->id); 210 | $this->qty = array_get($attributes, 'qty', $this->qty); 211 | $this->name = array_get($attributes, 'name', $this->name); 212 | $this->price = array_get($attributes, 'price', $this->price); 213 | $this->priceTax = $this->price + $this->tax; 214 | $this->options = new CartItemOptions(array_get($attributes, 'options', $this->options)); 215 | 216 | $this->rowId = $this->generateRowId($this->id, $this->options->all()); 217 | } 218 | 219 | /** 220 | * Associate the cart item with the given model. 221 | * 222 | * @param mixed $model 223 | * @return \Gloudemans\Shoppingcart\CartItem 224 | */ 225 | public function associate($model) 226 | { 227 | $this->associatedModel = is_string($model) ? $model : get_class($model); 228 | 229 | return $this; 230 | } 231 | 232 | /** 233 | * Set the tax rate. 234 | * 235 | * @param int|float $taxRate 236 | * @return \Gloudemans\Shoppingcart\CartItem 237 | */ 238 | public function setTaxRate($taxRate) 239 | { 240 | $this->taxRate = $taxRate; 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * Get an attribute from the cart item or get the associated model. 247 | * 248 | * @param string $attribute 249 | * @return mixed 250 | */ 251 | public function __get($attribute) 252 | { 253 | if(property_exists($this, $attribute)) { 254 | return $this->{$attribute}; 255 | } 256 | 257 | if($attribute === 'priceTax') { 258 | return $this->price + $this->tax; 259 | } 260 | 261 | if($attribute === 'subtotal') { 262 | return $this->qty * $this->price; 263 | } 264 | 265 | if($attribute === 'total') { 266 | return $this->qty * ($this->priceTax); 267 | } 268 | 269 | if($attribute === 'tax') { 270 | return $this->price * ($this->taxRate / 100); 271 | } 272 | 273 | if($attribute === 'taxTotal') { 274 | return $this->tax * $this->qty; 275 | } 276 | 277 | if($attribute === 'model' && isset($this->associatedModel)) { 278 | return with(new $this->associatedModel)->find($this->id); 279 | } 280 | 281 | return null; 282 | } 283 | 284 | /** 285 | * Create a new instance from a Buyable. 286 | * 287 | * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item 288 | * @param array $options 289 | * @return \Gloudemans\Shoppingcart\CartItem 290 | */ 291 | public static function fromBuyable(Buyable $item, array $options = []) 292 | { 293 | return new self($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $options); 294 | } 295 | 296 | /** 297 | * Create a new instance from the given array. 298 | * 299 | * @param array $attributes 300 | * @return \Gloudemans\Shoppingcart\CartItem 301 | */ 302 | public static function fromArray(array $attributes) 303 | { 304 | $options = array_get($attributes, 'options', []); 305 | 306 | return new self($attributes['id'], $attributes['name'], $attributes['price'], $options); 307 | } 308 | 309 | /** 310 | * Create a new instance from the given attributes. 311 | * 312 | * @param int|string $id 313 | * @param string $name 314 | * @param float $price 315 | * @param array $options 316 | * @return \Gloudemans\Shoppingcart\CartItem 317 | */ 318 | public static function fromAttributes($id, $name, $price, array $options = []) 319 | { 320 | return new self($id, $name, $price, $options); 321 | } 322 | 323 | /** 324 | * Generate a unique id for the cart item. 325 | * 326 | * @param string $id 327 | * @param array $options 328 | * @return string 329 | */ 330 | protected function generateRowId($id, array $options) 331 | { 332 | ksort($options); 333 | 334 | return md5($id . serialize($options)); 335 | } 336 | 337 | /** 338 | * Get the instance as an array. 339 | * 340 | * @return array 341 | */ 342 | public function toArray() 343 | { 344 | return [ 345 | 'rowId' => $this->rowId, 346 | 'id' => $this->id, 347 | 'name' => $this->name, 348 | 'qty' => $this->qty, 349 | 'price' => $this->price, 350 | 'options' => $this->options->toArray(), 351 | 'tax' => $this->tax, 352 | 'subtotal' => $this->subtotal 353 | ]; 354 | } 355 | 356 | /** 357 | * Convert the object to its JSON representation. 358 | * 359 | * @param int $options 360 | * @return string 361 | */ 362 | public function toJson($options = 0) 363 | { 364 | return json_encode($this->toArray(), $options); 365 | } 366 | 367 | /** 368 | * Get the formatted number. 369 | * 370 | * @param float $value 371 | * @param int $decimals 372 | * @param string $decimalPoint 373 | * @param string $thousandSeperator 374 | * @return string 375 | */ 376 | private function numberFormat($value, $decimals, $decimalPoint, $thousandSeperator) 377 | { 378 | if (is_null($decimals)){ 379 | $decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals'); 380 | } 381 | 382 | if (is_null($decimalPoint)){ 383 | $decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point'); 384 | } 385 | 386 | if (is_null($thousandSeperator)){ 387 | $thousandSeperator = is_null(config('cart.format.thousand_seperator')) ? ',' : config('cart.format.thousand_seperator'); 388 | } 389 | 390 | return number_format($value, $decimals, $decimalPoint, $thousandSeperator); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/CartItemOptions.php: -------------------------------------------------------------------------------- 1 | get($key); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Contracts/Buyable.php: -------------------------------------------------------------------------------- 1 | app->bind('cart', 'Gloudemans\Shoppingcart\Cart'); 20 | 21 | $config = __DIR__ . '/../config/cart.php'; 22 | $this->mergeConfigFrom($config, 'cart'); 23 | 24 | $this->publishes([__DIR__ . '/../config/cart.php' => config_path('cart.php')], 'config'); 25 | 26 | $this->app['events']->listen(Logout::class, function () { 27 | if ($this->app['config']->get('cart.destroy_on_logout')) { 28 | $this->app->make(SessionManager::class)->forget('cart'); 29 | } 30 | }); 31 | 32 | if ( ! class_exists('CreateShoppingcartTable')) { 33 | // Publish the migration 34 | $timestamp = date('Y_m_d_His', time()); 35 | 36 | $this->publishes([ 37 | __DIR__.'/../database/migrations/0000_00_00_000000_create_shoppingcart_table.php' => database_path('migrations/'.$timestamp.'_create_shoppingcart_table.php'), 38 | ], 'migrations'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/CartAssertions.php: -------------------------------------------------------------------------------- 1 | count(); 19 | 20 | PHPUnit::assertEquals($items, $cart->count(), "Expected the cart to contain {$items} items, but got {$actual}."); 21 | } 22 | 23 | /** 24 | * Assert that the cart contains the given number of rows. 25 | * 26 | * @param int $rows 27 | * @param \Gloudemans\Shoppingcart\Cart $cart 28 | */ 29 | public function assertRowsInCart($rows, Cart $cart) 30 | { 31 | $actual = $cart->content()->count(); 32 | 33 | PHPUnit::assertCount($rows, $cart->content(), "Expected the cart to contain {$rows} rows, but got {$actual}."); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/CartItemTest.php: -------------------------------------------------------------------------------- 1 | 'XL', 'color' => 'red']); 26 | $cartItem->setQuantity(2); 27 | 28 | $this->assertEquals([ 29 | 'id' => 1, 30 | 'name' => 'Some item', 31 | 'price' => 10.00, 32 | 'rowId' => '07d5da5550494c62daf9993cf954303f', 33 | 'qty' => 2, 34 | 'options' => [ 35 | 'size' => 'XL', 36 | 'color' => 'red' 37 | ], 38 | 'tax' => 0, 39 | 'subtotal' => 20.00, 40 | ], $cartItem->toArray()); 41 | } 42 | 43 | /** @test */ 44 | public function it_can_be_cast_to_json() 45 | { 46 | $cartItem = new CartItem(1, 'Some item', 10.00, ['size' => 'XL', 'color' => 'red']); 47 | $cartItem->setQuantity(2); 48 | 49 | $this->assertJson($cartItem->toJson()); 50 | 51 | $json = '{"rowId":"07d5da5550494c62daf9993cf954303f","id":1,"name":"Some item","qty":2,"price":10,"options":{"size":"XL","color":"red"},"tax":0,"subtotal":20}'; 52 | 53 | $this->assertEquals($json, $cartItem->toJson()); 54 | } 55 | } -------------------------------------------------------------------------------- /tests/CartTest.php: -------------------------------------------------------------------------------- 1 | set('cart.database.connection', 'testing'); 43 | 44 | $app['config']->set('session.driver', 'array'); 45 | 46 | $app['config']->set('database.default', 'testing'); 47 | $app['config']->set('database.connections.testing', [ 48 | 'driver' => 'sqlite', 49 | 'database' => ':memory:', 50 | 'prefix' => '', 51 | ]); 52 | } 53 | 54 | /** 55 | * Setup the test environment. 56 | * 57 | * @return void 58 | */ 59 | protected function setUp() 60 | { 61 | parent::setUp(); 62 | 63 | $this->app->afterResolving('migrator', function ($migrator) { 64 | $migrator->path(realpath(__DIR__.'/../database/migrations')); 65 | }); 66 | } 67 | 68 | /** @test */ 69 | public function it_has_a_default_instance() 70 | { 71 | $cart = $this->getCart(); 72 | 73 | $this->assertEquals(Cart::DEFAULT_INSTANCE, $cart->currentInstance()); 74 | } 75 | 76 | /** @test */ 77 | public function it_can_have_multiple_instances() 78 | { 79 | $cart = $this->getCart(); 80 | 81 | $cart->add(new BuyableProduct(1, 'First item')); 82 | 83 | $cart->instance('wishlist')->add(new BuyableProduct(2, 'Second item')); 84 | 85 | $this->assertItemsInCart(1, $cart->instance(Cart::DEFAULT_INSTANCE)); 86 | $this->assertItemsInCart(1, $cart->instance('wishlist')); 87 | } 88 | 89 | /** @test */ 90 | public function it_can_add_an_item() 91 | { 92 | Event::fake(); 93 | 94 | $cart = $this->getCart(); 95 | 96 | $cart->add(new BuyableProduct); 97 | 98 | $this->assertEquals(1, $cart->count()); 99 | 100 | Event::assertDispatched('cart.added'); 101 | } 102 | 103 | /** @test */ 104 | public function it_will_return_the_cartitem_of_the_added_item() 105 | { 106 | Event::fake(); 107 | 108 | $cart = $this->getCart(); 109 | 110 | $cartItem = $cart->add(new BuyableProduct); 111 | 112 | $this->assertInstanceOf(CartItem::class, $cartItem); 113 | $this->assertEquals('027c91341fd5cf4d2579b49c4b6a90da', $cartItem->rowId); 114 | 115 | Event::assertDispatched('cart.added'); 116 | } 117 | 118 | /** @test */ 119 | public function it_can_add_multiple_buyable_items_at_once() 120 | { 121 | Event::fake(); 122 | 123 | $cart = $this->getCart(); 124 | 125 | $cart->add([new BuyableProduct(1), new BuyableProduct(2)]); 126 | 127 | $this->assertEquals(2, $cart->count()); 128 | 129 | Event::assertDispatched('cart.added'); 130 | } 131 | 132 | /** @test */ 133 | public function it_will_return_an_array_of_cartitems_when_you_add_multiple_items_at_once() 134 | { 135 | Event::fake(); 136 | 137 | $cart = $this->getCart(); 138 | 139 | $cartItems = $cart->add([new BuyableProduct(1), new BuyableProduct(2)]); 140 | 141 | $this->assertTrue(is_array($cartItems)); 142 | $this->assertCount(2, $cartItems); 143 | $this->assertContainsOnlyInstancesOf(CartItem::class, $cartItems); 144 | 145 | Event::assertDispatched('cart.added'); 146 | } 147 | 148 | /** @test */ 149 | public function it_can_add_an_item_from_attributes() 150 | { 151 | Event::fake(); 152 | 153 | $cart = $this->getCart(); 154 | 155 | $cart->add(1, 'Test item', 1, 10.00); 156 | 157 | $this->assertEquals(1, $cart->count()); 158 | 159 | Event::assertDispatched('cart.added'); 160 | } 161 | 162 | /** @test */ 163 | public function it_can_add_an_item_from_an_array() 164 | { 165 | Event::fake(); 166 | 167 | $cart = $this->getCart(); 168 | 169 | $cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => 10.00]); 170 | 171 | $this->assertEquals(1, $cart->count()); 172 | 173 | Event::assertDispatched('cart.added'); 174 | } 175 | 176 | /** @test */ 177 | public function it_can_add_multiple_array_items_at_once() 178 | { 179 | Event::fake(); 180 | 181 | $cart = $this->getCart(); 182 | 183 | $cart->add([ 184 | ['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => 10.00], 185 | ['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => 10.00] 186 | ]); 187 | 188 | $this->assertEquals(2, $cart->count()); 189 | 190 | Event::assertDispatched('cart.added'); 191 | } 192 | 193 | /** @test */ 194 | public function it_can_add_an_item_with_options() 195 | { 196 | Event::fake(); 197 | 198 | $cart = $this->getCart(); 199 | 200 | $options = ['size' => 'XL', 'color' => 'red']; 201 | 202 | $cart->add(new BuyableProduct, 1, $options); 203 | 204 | $cartItem = $cart->get('07d5da5550494c62daf9993cf954303f'); 205 | 206 | $this->assertInstanceOf(CartItem::class, $cartItem); 207 | $this->assertEquals('XL', $cartItem->options->size); 208 | $this->assertEquals('red', $cartItem->options->color); 209 | 210 | Event::assertDispatched('cart.added'); 211 | } 212 | 213 | /** 214 | * @test 215 | * @expectedException \InvalidArgumentException 216 | * @expectedExceptionMessage Please supply a valid identifier. 217 | */ 218 | public function it_will_validate_the_identifier() 219 | { 220 | $cart = $this->getCart(); 221 | 222 | $cart->add(null, 'Some title', 1, 10.00); 223 | } 224 | 225 | /** 226 | * @test 227 | * @expectedException \InvalidArgumentException 228 | * @expectedExceptionMessage Please supply a valid name. 229 | */ 230 | public function it_will_validate_the_name() 231 | { 232 | $cart = $this->getCart(); 233 | 234 | $cart->add(1, null, 1, 10.00); 235 | } 236 | 237 | /** 238 | * @test 239 | * @expectedException \InvalidArgumentException 240 | * @expectedExceptionMessage Please supply a valid quantity. 241 | */ 242 | public function it_will_validate_the_quantity() 243 | { 244 | $cart = $this->getCart(); 245 | 246 | $cart->add(1, 'Some title', 'invalid', 10.00); 247 | } 248 | 249 | /** 250 | * @test 251 | * @expectedException \InvalidArgumentException 252 | * @expectedExceptionMessage Please supply a valid price. 253 | */ 254 | public function it_will_validate_the_price() 255 | { 256 | $cart = $this->getCart(); 257 | 258 | $cart->add(1, 'Some title', 1, 'invalid'); 259 | } 260 | 261 | /** @test */ 262 | public function it_will_update_the_cart_if_the_item_already_exists_in_the_cart() 263 | { 264 | $cart = $this->getCart(); 265 | 266 | $item = new BuyableProduct; 267 | 268 | $cart->add($item); 269 | $cart->add($item); 270 | 271 | $this->assertItemsInCart(2, $cart); 272 | $this->assertRowsInCart(1, $cart); 273 | } 274 | 275 | /** @test */ 276 | public function it_will_keep_updating_the_quantity_when_an_item_is_added_multiple_times() 277 | { 278 | $cart = $this->getCart(); 279 | 280 | $item = new BuyableProduct; 281 | 282 | $cart->add($item); 283 | $cart->add($item); 284 | $cart->add($item); 285 | 286 | $this->assertItemsInCart(3, $cart); 287 | $this->assertRowsInCart(1, $cart); 288 | } 289 | 290 | /** @test */ 291 | public function it_can_update_the_quantity_of_an_existing_item_in_the_cart() 292 | { 293 | Event::fake(); 294 | 295 | $cart = $this->getCart(); 296 | 297 | $cart->add(new BuyableProduct); 298 | 299 | $cart->update('027c91341fd5cf4d2579b49c4b6a90da', 2); 300 | 301 | $this->assertItemsInCart(2, $cart); 302 | $this->assertRowsInCart(1, $cart); 303 | 304 | Event::assertDispatched('cart.updated'); 305 | } 306 | 307 | /** @test */ 308 | public function it_can_update_an_existing_item_in_the_cart_from_a_buyable() 309 | { 310 | Event::fake(); 311 | 312 | $cart = $this->getCart(); 313 | 314 | $cart->add(new BuyableProduct); 315 | 316 | $cart->update('027c91341fd5cf4d2579b49c4b6a90da', new BuyableProduct(1, 'Different description')); 317 | 318 | $this->assertItemsInCart(1, $cart); 319 | $this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name); 320 | 321 | Event::assertDispatched('cart.updated'); 322 | } 323 | 324 | /** @test */ 325 | public function it_can_update_an_existing_item_in_the_cart_from_an_array() 326 | { 327 | Event::fake(); 328 | 329 | $cart = $this->getCart(); 330 | 331 | $cart->add(new BuyableProduct); 332 | 333 | $cart->update('027c91341fd5cf4d2579b49c4b6a90da', ['name' => 'Different description']); 334 | 335 | $this->assertItemsInCart(1, $cart); 336 | $this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name); 337 | 338 | Event::assertDispatched('cart.updated'); 339 | } 340 | 341 | /** 342 | * @test 343 | * @expectedException \Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException 344 | */ 345 | public function it_will_throw_an_exception_if_a_rowid_was_not_found() 346 | { 347 | $cart = $this->getCart(); 348 | 349 | $cart->add(new BuyableProduct); 350 | 351 | $cart->update('none-existing-rowid', new BuyableProduct(1, 'Different description')); 352 | } 353 | 354 | /** @test */ 355 | public function it_will_regenerate_the_rowid_if_the_options_changed() 356 | { 357 | $cart = $this->getCart(); 358 | 359 | $cart->add(new BuyableProduct, 1, ['color' => 'red']); 360 | 361 | $cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => ['color' => 'blue']]); 362 | 363 | $this->assertItemsInCart(1, $cart); 364 | $this->assertEquals('7e70a1e9aaadd18c72921a07aae5d011', $cart->content()->first()->rowId); 365 | $this->assertEquals('blue', $cart->get('7e70a1e9aaadd18c72921a07aae5d011')->options->color); 366 | } 367 | 368 | /** @test */ 369 | public function it_will_add_the_item_to_an_existing_row_if_the_options_changed_to_an_existing_rowid() 370 | { 371 | $cart = $this->getCart(); 372 | 373 | $cart->add(new BuyableProduct, 1, ['color' => 'red']); 374 | $cart->add(new BuyableProduct, 1, ['color' => 'blue']); 375 | 376 | $cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => ['color' => 'red']]); 377 | 378 | $this->assertItemsInCart(2, $cart); 379 | $this->assertRowsInCart(1, $cart); 380 | } 381 | 382 | /** @test */ 383 | public function it_can_remove_an_item_from_the_cart() 384 | { 385 | Event::fake(); 386 | 387 | $cart = $this->getCart(); 388 | 389 | $cart->add(new BuyableProduct); 390 | 391 | $cart->remove('027c91341fd5cf4d2579b49c4b6a90da'); 392 | 393 | $this->assertItemsInCart(0, $cart); 394 | $this->assertRowsInCart(0, $cart); 395 | 396 | Event::assertDispatched('cart.removed'); 397 | } 398 | 399 | /** @test */ 400 | public function it_will_remove_the_item_if_its_quantity_was_set_to_zero() 401 | { 402 | Event::fake(); 403 | 404 | $cart = $this->getCart(); 405 | 406 | $cart->add(new BuyableProduct); 407 | 408 | $cart->update('027c91341fd5cf4d2579b49c4b6a90da', 0); 409 | 410 | $this->assertItemsInCart(0, $cart); 411 | $this->assertRowsInCart(0, $cart); 412 | 413 | Event::assertDispatched('cart.removed'); 414 | } 415 | 416 | /** @test */ 417 | public function it_will_remove_the_item_if_its_quantity_was_set_negative() 418 | { 419 | Event::fake(); 420 | 421 | $cart = $this->getCart(); 422 | 423 | $cart->add(new BuyableProduct); 424 | 425 | $cart->update('027c91341fd5cf4d2579b49c4b6a90da', -1); 426 | 427 | $this->assertItemsInCart(0, $cart); 428 | $this->assertRowsInCart(0, $cart); 429 | 430 | Event::assertDispatched('cart.removed'); 431 | } 432 | 433 | /** @test */ 434 | public function it_can_get_an_item_from_the_cart_by_its_rowid() 435 | { 436 | $cart = $this->getCart(); 437 | 438 | $cart->add(new BuyableProduct); 439 | 440 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 441 | 442 | $this->assertInstanceOf(CartItem::class, $cartItem); 443 | } 444 | 445 | /** @test */ 446 | public function it_can_get_the_content_of_the_cart() 447 | { 448 | $cart = $this->getCart(); 449 | 450 | $cart->add(new BuyableProduct(1)); 451 | $cart->add(new BuyableProduct(2)); 452 | 453 | $content = $cart->content(); 454 | 455 | $this->assertInstanceOf(Collection::class, $content); 456 | $this->assertCount(2, $content); 457 | } 458 | 459 | /** @test */ 460 | public function it_will_return_an_empty_collection_if_the_cart_is_empty() 461 | { 462 | $cart = $this->getCart(); 463 | 464 | $content = $cart->content(); 465 | 466 | $this->assertInstanceOf(Collection::class, $content); 467 | $this->assertCount(0, $content); 468 | } 469 | 470 | /** @test */ 471 | public function it_will_include_the_tax_and_subtotal_when_converted_to_an_array() 472 | { 473 | $cart = $this->getCart(); 474 | 475 | $cart->add(new BuyableProduct(1)); 476 | $cart->add(new BuyableProduct(2)); 477 | 478 | $content = $cart->content(); 479 | 480 | $this->assertInstanceOf(Collection::class, $content); 481 | $this->assertEquals([ 482 | '027c91341fd5cf4d2579b49c4b6a90da' => [ 483 | 'rowId' => '027c91341fd5cf4d2579b49c4b6a90da', 484 | 'id' => 1, 485 | 'name' => 'Item name', 486 | 'qty' => 1, 487 | 'price' => 10.00, 488 | 'tax' => 2.10, 489 | 'subtotal' => 10.0, 490 | 'options' => [], 491 | ], 492 | '370d08585360f5c568b18d1f2e4ca1df' => [ 493 | 'rowId' => '370d08585360f5c568b18d1f2e4ca1df', 494 | 'id' => 2, 495 | 'name' => 'Item name', 496 | 'qty' => 1, 497 | 'price' => 10.00, 498 | 'tax' => 2.10, 499 | 'subtotal' => 10.0, 500 | 'options' => [], 501 | ] 502 | ], $content->toArray()); 503 | } 504 | 505 | /** @test */ 506 | public function it_can_destroy_a_cart() 507 | { 508 | $cart = $this->getCart(); 509 | 510 | $cart->add(new BuyableProduct); 511 | 512 | $this->assertItemsInCart(1, $cart); 513 | 514 | $cart->destroy(); 515 | 516 | $this->assertItemsInCart(0, $cart); 517 | } 518 | 519 | /** @test */ 520 | public function it_can_get_the_total_price_of_the_cart_content() 521 | { 522 | $cart = $this->getCart(); 523 | 524 | $cart->add(new BuyableProduct(1, 'First item', 10.00)); 525 | $cart->add(new BuyableProduct(2, 'Second item', 25.00), 2); 526 | 527 | $this->assertItemsInCart(3, $cart); 528 | $this->assertEquals(60.00, $cart->subtotal()); 529 | } 530 | 531 | /** @test */ 532 | public function it_can_return_a_formatted_total() 533 | { 534 | $cart = $this->getCart(); 535 | 536 | $cart->add(new BuyableProduct(1, 'First item', 1000.00)); 537 | $cart->add(new BuyableProduct(2, 'Second item', 2500.00), 2); 538 | 539 | $this->assertItemsInCart(3, $cart); 540 | $this->assertEquals('6.000,00', $cart->subtotal(2, ',', '.')); 541 | } 542 | 543 | /** @test */ 544 | public function it_can_search_the_cart_for_a_specific_item() 545 | { 546 | $cart = $this->getCart(); 547 | 548 | $cart->add(new BuyableProduct(1, 'Some item')); 549 | $cart->add(new BuyableProduct(2, 'Another item')); 550 | 551 | $cartItem = $cart->search(function ($cartItem, $rowId) { 552 | return $cartItem->name == 'Some item'; 553 | }); 554 | 555 | $this->assertInstanceOf(Collection::class, $cartItem); 556 | $this->assertCount(1, $cartItem); 557 | $this->assertInstanceOf(CartItem::class, $cartItem->first()); 558 | $this->assertEquals(1, $cartItem->first()->id); 559 | } 560 | 561 | /** @test */ 562 | public function it_can_search_the_cart_for_multiple_items() 563 | { 564 | $cart = $this->getCart(); 565 | 566 | $cart->add(new BuyableProduct(1, 'Some item')); 567 | $cart->add(new BuyableProduct(2, 'Some item')); 568 | $cart->add(new BuyableProduct(3, 'Another item')); 569 | 570 | $cartItem = $cart->search(function ($cartItem, $rowId) { 571 | return $cartItem->name == 'Some item'; 572 | }); 573 | 574 | $this->assertInstanceOf(Collection::class, $cartItem); 575 | } 576 | 577 | /** @test */ 578 | public function it_can_search_the_cart_for_a_specific_item_with_options() 579 | { 580 | $cart = $this->getCart(); 581 | 582 | $cart->add(new BuyableProduct(1, 'Some item'), 1, ['color' => 'red']); 583 | $cart->add(new BuyableProduct(2, 'Another item'), 1, ['color' => 'blue']); 584 | 585 | $cartItem = $cart->search(function ($cartItem, $rowId) { 586 | return $cartItem->options->color == 'red'; 587 | }); 588 | 589 | $this->assertInstanceOf(Collection::class, $cartItem); 590 | $this->assertCount(1, $cartItem); 591 | $this->assertInstanceOf(CartItem::class, $cartItem->first()); 592 | $this->assertEquals(1, $cartItem->first()->id); 593 | } 594 | 595 | /** @test */ 596 | public function it_will_associate_the_cart_item_with_a_model_when_you_add_a_buyable() 597 | { 598 | $cart = $this->getCart(); 599 | 600 | $cart->add(new BuyableProduct); 601 | 602 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 603 | 604 | $this->assertContains(BuyableProduct::class, Assert::readAttribute($cartItem, 'associatedModel')); 605 | } 606 | 607 | /** @test */ 608 | public function it_can_associate_the_cart_item_with_a_model() 609 | { 610 | $cart = $this->getCart(); 611 | 612 | $cart->add(1, 'Test item', 1, 10.00); 613 | 614 | $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel); 615 | 616 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 617 | 618 | $this->assertEquals(ProductModel::class, Assert::readAttribute($cartItem, 'associatedModel')); 619 | } 620 | 621 | /** 622 | * @test 623 | * @expectedException \Gloudemans\Shoppingcart\Exceptions\UnknownModelException 624 | * @expectedExceptionMessage The supplied model SomeModel does not exist. 625 | */ 626 | public function it_will_throw_an_exception_when_a_non_existing_model_is_being_associated() 627 | { 628 | $cart = $this->getCart(); 629 | 630 | $cart->add(1, 'Test item', 1, 10.00); 631 | 632 | $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', 'SomeModel'); 633 | } 634 | 635 | /** @test */ 636 | public function it_can_get_the_associated_model_of_a_cart_item() 637 | { 638 | $cart = $this->getCart(); 639 | 640 | $cart->add(1, 'Test item', 1, 10.00); 641 | 642 | $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel); 643 | 644 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 645 | 646 | $this->assertInstanceOf(ProductModel::class, $cartItem->model); 647 | $this->assertEquals('Some value', $cartItem->model->someValue); 648 | } 649 | 650 | /** @test */ 651 | public function it_can_calculate_the_subtotal_of_a_cart_item() 652 | { 653 | $cart = $this->getCart(); 654 | 655 | $cart->add(new BuyableProduct(1, 'Some title', 9.99), 3); 656 | 657 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 658 | 659 | $this->assertEquals(29.97, $cartItem->subtotal); 660 | } 661 | 662 | /** @test */ 663 | public function it_can_return_a_formatted_subtotal() 664 | { 665 | $cart = $this->getCart(); 666 | 667 | $cart->add(new BuyableProduct(1, 'Some title', 500), 3); 668 | 669 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 670 | 671 | $this->assertEquals('1.500,00', $cartItem->subtotal(2, ',', '.')); 672 | } 673 | 674 | /** @test */ 675 | public function it_can_calculate_tax_based_on_the_default_tax_rate_in_the_config() 676 | { 677 | $cart = $this->getCart(); 678 | 679 | $cart->add(new BuyableProduct(1, 'Some title', 10.00), 1); 680 | 681 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 682 | 683 | $this->assertEquals(2.10, $cartItem->tax); 684 | } 685 | 686 | /** @test */ 687 | public function it_can_calculate_tax_based_on_the_specified_tax() 688 | { 689 | $cart = $this->getCart(); 690 | 691 | $cart->add(new BuyableProduct(1, 'Some title', 10.00), 1); 692 | 693 | $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); 694 | 695 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 696 | 697 | $this->assertEquals(1.90, $cartItem->tax); 698 | } 699 | 700 | /** @test */ 701 | public function it_can_return_the_calculated_tax_formatted() 702 | { 703 | $cart = $this->getCart(); 704 | 705 | $cart->add(new BuyableProduct(1, 'Some title', 10000.00), 1); 706 | 707 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 708 | 709 | $this->assertEquals('2.100,00', $cartItem->tax(2, ',', '.')); 710 | } 711 | 712 | /** @test */ 713 | public function it_can_calculate_the_total_tax_for_all_cart_items() 714 | { 715 | $cart = $this->getCart(); 716 | 717 | $cart->add(new BuyableProduct(1, 'Some title', 10.00), 1); 718 | $cart->add(new BuyableProduct(2, 'Some title', 20.00), 2); 719 | 720 | $this->assertEquals(10.50, $cart->tax); 721 | } 722 | 723 | /** @test */ 724 | public function it_can_return_formatted_total_tax() 725 | { 726 | $cart = $this->getCart(); 727 | 728 | $cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1); 729 | $cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2); 730 | 731 | $this->assertEquals('1.050,00', $cart->tax(2, ',', '.')); 732 | } 733 | 734 | /** @test */ 735 | public function it_can_return_the_subtotal() 736 | { 737 | $cart = $this->getCart(); 738 | 739 | $cart->add(new BuyableProduct(1, 'Some title', 10.00), 1); 740 | $cart->add(new BuyableProduct(2, 'Some title', 20.00), 2); 741 | 742 | $this->assertEquals(50.00, $cart->subtotal); 743 | } 744 | 745 | /** @test */ 746 | public function it_can_return_formatted_subtotal() 747 | { 748 | $cart = $this->getCart(); 749 | 750 | $cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1); 751 | $cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2); 752 | 753 | $this->assertEquals('5000,00', $cart->subtotal(2, ',', '')); 754 | } 755 | 756 | /** @test */ 757 | public function it_can_return_cart_formated_numbers_by_config_values() 758 | { 759 | $this->setConfigFormat(2, ',', ''); 760 | 761 | $cart = $this->getCart(); 762 | 763 | $cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1); 764 | $cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2); 765 | 766 | $this->assertEquals('5000,00', $cart->subtotal()); 767 | $this->assertEquals('1050,00', $cart->tax()); 768 | $this->assertEquals('6050,00', $cart->total()); 769 | 770 | $this->assertEquals('5000,00', $cart->subtotal); 771 | $this->assertEquals('1050,00', $cart->tax); 772 | $this->assertEquals('6050,00', $cart->total); 773 | } 774 | 775 | /** @test */ 776 | public function it_can_return_cartItem_formated_numbers_by_config_values() 777 | { 778 | $this->setConfigFormat(2, ',', ''); 779 | 780 | $cart = $this->getCart(); 781 | 782 | $cart->add(new BuyableProduct(1, 'Some title', 2000.00), 2); 783 | 784 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 785 | 786 | $this->assertEquals('2000,00', $cartItem->price()); 787 | $this->assertEquals('2420,00', $cartItem->priceTax()); 788 | $this->assertEquals('4000,00', $cartItem->subtotal()); 789 | $this->assertEquals('4840,00', $cartItem->total()); 790 | $this->assertEquals('420,00', $cartItem->tax()); 791 | $this->assertEquals('840,00', $cartItem->taxTotal()); 792 | } 793 | 794 | /** @test */ 795 | public function it_can_store_the_cart_in_a_database() 796 | { 797 | $this->artisan('migrate', [ 798 | '--database' => 'testing', 799 | ]); 800 | 801 | Event::fake(); 802 | 803 | $cart = $this->getCart(); 804 | 805 | $cart->add(new BuyableProduct); 806 | 807 | $cart->store($identifier = 123); 808 | 809 | $serialized = serialize($cart->content()); 810 | 811 | $this->assertDatabaseHas('shoppingcart', ['identifier' => $identifier, 'instance' => 'default', 'content' => $serialized]); 812 | 813 | Event::assertDispatched('cart.stored'); 814 | } 815 | 816 | /** 817 | * @test 818 | * @expectedException \Gloudemans\Shoppingcart\Exceptions\CartAlreadyStoredException 819 | * @expectedExceptionMessage A cart with identifier 123 was already stored. 820 | */ 821 | public function it_will_throw_an_exception_when_a_cart_was_already_stored_using_the_specified_identifier() 822 | { 823 | $this->artisan('migrate', [ 824 | '--database' => 'testing', 825 | ]); 826 | 827 | Event::fake(); 828 | 829 | $cart = $this->getCart(); 830 | 831 | $cart->add(new BuyableProduct); 832 | 833 | $cart->store($identifier = 123); 834 | 835 | $cart->store($identifier); 836 | 837 | Event::assertDispatched('cart.stored'); 838 | } 839 | 840 | /** @test */ 841 | public function it_can_restore_a_cart_from_the_database() 842 | { 843 | $this->artisan('migrate', [ 844 | '--database' => 'testing', 845 | ]); 846 | 847 | Event::fake(); 848 | 849 | $cart = $this->getCart(); 850 | 851 | $cart->add(new BuyableProduct); 852 | 853 | $cart->store($identifier = 123); 854 | 855 | $cart->destroy(); 856 | 857 | $this->assertItemsInCart(0, $cart); 858 | 859 | $cart->restore($identifier); 860 | 861 | $this->assertItemsInCart(1, $cart); 862 | 863 | $this->assertDatabaseMissing('shoppingcart', ['identifier' => $identifier, 'instance' => 'default']); 864 | 865 | Event::assertDispatched('cart.restored'); 866 | } 867 | 868 | /** @test */ 869 | public function it_will_just_keep_the_current_instance_if_no_cart_with_the_given_identifier_was_stored() 870 | { 871 | $this->artisan('migrate', [ 872 | '--database' => 'testing', 873 | ]); 874 | 875 | $cart = $this->getCart(); 876 | 877 | $cart->restore($identifier = 123); 878 | 879 | $this->assertItemsInCart(0, $cart); 880 | } 881 | 882 | /** @test */ 883 | public function it_can_calculate_all_values() 884 | { 885 | $cart = $this->getCart(); 886 | 887 | $cart->add(new BuyableProduct(1, 'First item', 10.00), 2); 888 | 889 | $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); 890 | 891 | $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); 892 | 893 | $this->assertEquals(10.00, $cartItem->price(2)); 894 | $this->assertEquals(11.90, $cartItem->priceTax(2)); 895 | $this->assertEquals(20.00, $cartItem->subtotal(2)); 896 | $this->assertEquals(23.80, $cartItem->total(2)); 897 | $this->assertEquals(1.90, $cartItem->tax(2)); 898 | $this->assertEquals(3.80, $cartItem->taxTotal(2)); 899 | 900 | $this->assertEquals(20.00, $cart->subtotal(2)); 901 | $this->assertEquals(23.80, $cart->total(2)); 902 | $this->assertEquals(3.80, $cart->tax(2)); 903 | } 904 | 905 | /** @test */ 906 | public function it_will_destroy_the_cart_when_the_user_logs_out_and_the_config_setting_was_set_to_true() 907 | { 908 | $this->app['config']->set('cart.destroy_on_logout', true); 909 | 910 | $this->app->instance(SessionManager::class, Mockery::mock(SessionManager::class, function ($mock) { 911 | $mock->shouldReceive('forget')->once()->with('cart'); 912 | })); 913 | 914 | $user = Mockery::mock(Authenticatable::class); 915 | 916 | event(new Logout($user)); 917 | } 918 | 919 | /** 920 | * Get an instance of the cart. 921 | * 922 | * @return \Gloudemans\Shoppingcart\Cart 923 | */ 924 | private function getCart() 925 | { 926 | $session = $this->app->make('session'); 927 | $events = $this->app->make('events'); 928 | 929 | return new Cart($session, $events); 930 | } 931 | 932 | /** 933 | * Set the config number format. 934 | * 935 | * @param int $decimals 936 | * @param string $decimalPoint 937 | * @param string $thousandSeperator 938 | */ 939 | private function setConfigFormat($decimals, $decimalPoint, $thousandSeperator) 940 | { 941 | $this->app['config']->set('cart.format.decimals', $decimals); 942 | $this->app['config']->set('cart.format.decimal_point', $decimalPoint); 943 | $this->app['config']->set('cart.format.thousand_seperator', $thousandSeperator); 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /tests/Fixtures/BuyableProduct.php: -------------------------------------------------------------------------------- 1 | id = $id; 34 | $this->name = $name; 35 | $this->price = $price; 36 | } 37 | 38 | /** 39 | * Get the identifier of the Buyable item. 40 | * 41 | * @return int|string 42 | */ 43 | public function getBuyableIdentifier($options = null) 44 | { 45 | return $this->id; 46 | } 47 | 48 | /** 49 | * Get the description or title of the Buyable item. 50 | * 51 | * @return string 52 | */ 53 | public function getBuyableDescription($options = null) 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * Get the price of the Buyable item. 60 | * 61 | * @return float 62 | */ 63 | public function getBuyablePrice($options = null) 64 | { 65 | return $this->price; 66 | } 67 | } -------------------------------------------------------------------------------- /tests/Fixtures/ProductModel.php: -------------------------------------------------------------------------------- 1 |