This commit is contained in:
Kevin Adametz 2024-08-05 12:05:24 +02:00
parent 04d677d37a
commit bfa3bb1df4
1191 changed files with 637397 additions and 10619 deletions

View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.scss]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

View file

@ -0,0 +1,5 @@
/vendor
composer.phar
composer.lock
.DS_Store
/.idea

View file

@ -0,0 +1,10 @@
language: php
php:
- 7.0
before_script:
- composer self-update
- composer install --prefer-source --no-interaction
script: vendor/bin/phpunit

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Rob Gloudemans <info@robgloudemans.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,491 @@
## LaravelShoppingcart
[![License](https://poser.pugx.org/gloudemans/shoppingcart/license)](https://packagist.org/packages/gloudemans/shoppingcart)
A simple shoppingcart implementation for Laravel.
## Installation
Install the package through [Composer](http://getcomposer.org/).
Run the Composer require command from the Terminal:
composer require digital-bird/shoppingcart
If you're using Laravel 5.5, this is all there is to do.
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.
Add a new line to the `providers` array:
Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class
And optionally add a new line to the `aliases` array:
'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class,
Now you're ready to start using the shoppingcart in your application.
**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**
## Overview
Look at one of the following topics to learn more about LaravelShoppingcart
* [Usage](#usage)
* [Collections](#collections)
* [Instances](#instances)
* [Models](#models)
* [Database](#database)
* [Exceptions](#exceptions)
* [Events](#events)
* [Example](#example)
## Usage
The shoppingcart gives you the following methods to use:
### Cart::add()
Adding an item to the cart is really simple, you just use the `add()` method, which accepts a variety of parameters.
In its most basic form you can specify the id, name, quantity, price, and tax rate of the product you'd like to add to the cart.
```php
Cart::add('293ad', 'Product 1', 1, 9.99, 10);
```
As an optional sixth parameter you can pass it options, so you can add multiple items with the same id, but with (for instance) a different size.
The fifth parameter is the tax rate. Eg 10 for 10%.
```php
Cart::add('293ad', 'Product 1', 1, 9.99, 10, ['size' => 'large']);
```
**The `add()` method will return an CartItem instance of the item you just added to the cart.**
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.
```php
Cart::add(['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 9.99, 'taxRate' => 10, 'options' => ['size' => 'large']]);
```
New in version 2 of the package is the possibility to work with the [Buyable](#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.
This way you can just pass the `add()` method a model and the quantity and it will automatically add it to the cart.
The path to the `Buyable` interface is:
Gloudemans\Shoppingcart\Contracts\Buyable;
**As an added bonus it will automatically associate the model with the CartItem**
```php
Cart::add($product, 1, ['size' => 'large']);
```
As an optional third parameter you can add options.
```php
Cart::add($product, 1, ['size' => 'large']);
```
Finally, you can also add multipe items to the cart at once.
You can just pass the `add()` method an array of arrays, or an array of Buyables and they will be added to the cart.
**When adding multiple items to the cart, the `add()` method will return an array of CartItems.**
```php
Cart::add([
['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 10.00, 'taxRate' => 10],
['id' => '4832k', 'name' => 'Product 2', 'qty' => 1, 'price' => 10.00, 'taxRate' => 10, 'options' => ['size' => 'large']]
]);
Cart::add([$product1, $product2]);
```
### Cart::update()
To update an item in the cart, you'll first need the rowId of the item.
Next you can use the `update()` method to update it.
If you simply want to update the quantity, you'll pass the update method the rowId and the new quantity:
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::update($rowId, 2); // Will update the quantity
```
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.
```php
Cart::update($rowId, ['name' => 'Product 1']); // Will update the name
Cart::update($rowId, $product); // Will update the id, name and price
```
### Cart::remove()
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.
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::remove($rowId);
```
### Cart::get()
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.
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::get($rowId);
```
### Cart::content()
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.
```php
Cart::content();
```
This method will return the content of the current cart instance, if you want the content of another instance, simply chain the calls.
```php
Cart::instance('wishlist')->content();
```
### Cart::destroy()
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.
```php
Cart::destroy();
```
### Cart::total()
The `total()` method can be used to get the calculated total of all items in the cart, given there price and quantity.
```php
Cart::total();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::total($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::tax()
The `tax()` method can be used to get the calculated amount of tax for all items in the cart, given there price and quantity.
```php
Cart::tax();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::tax($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::subtotal()
The `subtotal()` method can be used to get the total of all items in the cart, minus the total amount of tax.
```php
Cart::subtotal();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::subtotal($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::count()
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.
```php
Cart::count();
```
### Cart::search()
To find an item in the cart, you can use the `search()` method.
**This method was changed on version 2**
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.
If you for instance want to find all items with an id of 1:
```php
$cart->search(function ($cartItem, $rowId) {
return $cartItem->id === 1;
});
```
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.
**The method will return a Collection containing all CartItems that where found**
This way of searching gives you total control over the search process and gives you the ability to create very precise and specific searches.
## Collections
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.
As an example, you can quicky get the number of unique products in a cart:
```php
Cart::content()->count();
```
Or you can group the content by the id of the products:
```php
Cart::content()->groupBy('id');
```
## Instances
The packages supports multiple instances of the cart. The way this works is like this:
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.
If you want to switch instances, you just call `Cart::instance('otherInstance')` again, and you're working with the `otherInstance` again.
So a little example:
```php
Cart::instance('shopping')->add('192ao12', 'Product 1', 1, 9.99);
// Get the content of the 'shopping' cart
Cart::content();
Cart::instance('wishlist')->add('sdjk922', 'Product 2', 1, 19.95, ['size' => 'medium']);
// Get the content of the 'wishlist' cart
Cart::content();
// If you want to get the content of the 'shopping' cart again
Cart::instance('shopping')->content();
// And the count of the 'wishlist' cart again
Cart::instance('wishlist')->count();
```
**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.**
**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()`.**
## Models
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.
That way you can access your model right from the `CartItem`!
The model can be accessed via the `model` property on the CartItem.
**If your model implements the `Buyable` interface and you used your model to add the item to the cart, it will associate automatically.**
Here is an example:
```php
// First we'll add the item to the cart.
$cartItem = Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large']);
// Next we associate a model with the item.
Cart::associate($cartItem->rowId, 'Product');
// Or even easier, call the associate method on the CartItem!
$cartItem->associate('Product');
// You can even make it a one-liner
Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large'])->associate('Product');
// Now, when iterating over the content of the cart, you can access the model.
foreach(Cart::content() as $row) {
echo 'You have ' . $row->qty . ' items of ' . $row->model->name . ' with description: "' . $row->model->description . '" in your cart.';
}
```
### Buyable
For the convenience of faster adding items to cart and their automatic association, your model can implement `Buyable` interface. To do so, it must implement such functions:
```php
public function getBuyableIdentifier(){
return $this->id;
}
public function getBuyableDescription(){
return $this->name;
}
public function getBuyablePrice(){
return $this->price;
}
```
Example:
```php
<?php
namespace App\Models;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Illuminate\Database\Eloquent\Model;
class Product exends Model implements Buyable {
public function getBuyableIdentifier($options = null) {
return $this->id;
}
public function getBuyableDescription($options = null) {
return $this->name;
}
public function getBuyablePrice($options = null) {
return $this->price;
}
}
```
## Database
- [Config](#configuration)
- [Storing the cart](#storing-the-cart)
- [Restoring the cart](#restoring-the-cart)
### Configuration
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.
By default the package will use the default database connection and use a table named `shoppingcart`.
If you want to change these options, you'll have to publish the `config` file.
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config"
This will give you a `cart.php` config file in which you can make the changes.
To make your life easy, the package also includes a ready to use `migration` which you can publish by running:
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="migrations"
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.
### Storing the cart
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.
Cart::store('username');
// To store a cart instance named 'wishlist'
Cart::instance('wishlist')->store('username');
### Restoring the cart
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.
Cart::restore('username');
// To restore a cart instance named 'wishlist'
Cart::instance('wishlist')->restore('username');
## Exceptions
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:
| Exception | Reason |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| *CartAlreadyStoredException* | When trying to store a cart that was already stored using the specified identifier |
| *InvalidRowIDException* | When the rowId that got passed doesn't exists in the current cart instance |
| *UnknownModelException* | When you try to associate an none existing model to a CartItem. |
## Events
The cart also has events build in. There are five events available for you to listen for.
| Event | Fired | Parameter |
| ------------- | ---------------------------------------- | -------------------------------- |
| cart.added | When an item was added to the cart. | The `CartItem` that was added. |
| cart.updated | When an item in the cart was updated. | The `CartItem` that was updated. |
| cart.removed | When an item is removed from the cart. | The `CartItem` that was removed. |
| cart.stored | When the content of a cart was stored. | - |
| cart.restored | When the content of a cart was restored. | - |
## Example
Below is a little example of how to list the cart content in a table:
```php
// Add some items in your Controller.
Cart::add('192ao12', 'Product 1', 1, 9.99);
Cart::add('1239ad0', 'Product 2', 2, 5.95, ['size' => 'large']);
// Display the content in a View.
<table>
<thead>
<tr>
<th>Product</th>
<th>Qty</th>
<th>Price</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<?php foreach(Cart::content() as $row) :?>
<tr>
<td>
<p><strong><?php echo $row->name; ?></strong></p>
<p><?php echo ($row->options->has('size') ? $row->options->size : ''); ?></p>
</td>
<td><input type="text" value="<?php echo $row->qty; ?>"></td>
<td>$<?php echo $row->price; ?></td>
<td>$<?php echo $row->total; ?></td>
</tr>
<?php endforeach;?>
</tbody>
<tfoot>
<tr>
<td colspan="2">&nbsp;</td>
<td>Subtotal</td>
<td><?php echo Cart::subtotal(); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Tax</td>
<td><?php echo Cart::tax(); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Total</td>
<td><?php echo Cart::total(); ?></td>
</tr>
</tfoot>
</table>
```

View file

@ -0,0 +1,49 @@
{
"name": "digital-bird/shoppingcart",
"description": "Laravel Shoppingcart",
"keywords": [
"laravel",
"shoppingcart"
],
"license": "MIT",
"authors": [
{
"name": "Rob Gloudemans",
"email": "info@robgloudemans.nl"
}
],
"require": {
"illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*|8.*",
"illuminate/session": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*|8.*",
"illuminate/events": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*|5.6.*|5.7.*|5.8.*|6.*|7.*|8.*"
},
"require-dev": {
"phpunit/phpunit": "~5.0|~6.0|~7.0|~8.0|~9,0",
"mockery/mockery": "~0.9.0",
"orchestra/testbench": "~3.1"
},
"autoload": {
"psr-4": {
"Gloudemans\\Shoppingcart\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Gloudemans\\Tests\\Shoppingcart\\": "tests/"
}
},
"suggest": {
"gloudemans/notify": "Simple flash notifications for Laravel"
},
"minimum-stability": "stable",
"extra": {
"laravel": {
"providers": [
"Gloudemans\\Shoppingcart\\ShoppingcartServiceProvider"
],
"aliases": {
"Cart": "Gloudemans\\Shoppingcart\\Facades\\Cart"
}
}
}
}

View file

@ -0,0 +1,105 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default tax rate
|--------------------------------------------------------------------------
|
| This default tax rate will be used when you make a class implement the
| Taxable interface and use the HasTax trait.
|
*/
'tax' => 10,
/*
|--------------------------------------------------------------------------
| Allow multiple rows with the same ID
|--------------------------------------------------------------------------
|
| By default, the row ID is generated to a MD5 based on ID + Options array.
| If you want to have more than one unique product in a cart, you either have
| to set a unique value in the Options array (eg Reason = X), or you can simply
| enable this option, and each time you add a new Cart row, it will add to the cart.
|
*/
'allow_multiple_same_id' => false,
/*
|--------------------------------------------------------------------------
| Shoppingcart database settings
|--------------------------------------------------------------------------
|
| Here you can set the connection that the shoppingcart should use when
| storing and restoring a cart.
|
*/
'database' => [
'connection' => null,
'table' => 'shoppingcart',
],
/*
|--------------------------------------------------------------------------
| Destroy the cart on user logout
|--------------------------------------------------------------------------
|
| When this option is set to 'true' the cart will automatically
| destroy all cart instances when the user logs out.
|
*/
'destroy_on_logout' => false,
/*
|--------------------------------------------------------------------------
| Default number format
|--------------------------------------------------------------------------
|
| This defaults will be used for the formated numbers if you don't
| set them in the method call.
|
*/
'format' => [
// General rule with decimal points is that if it's a unit price then it should be 2 decimals,
// as that's what is displayed to the user. Once it's multiplied by quantity, it should be 4 decimals to
// account for that multiplication of qty, and the total tax, subtotal inc tax, and total should all reflect that,
// and only round to show the user at the absolute final stage.
'price_ex_tax_decimals' => 2, // base unit price shown to user ex gst should be 2 by default.
'price_inc_tax_decimals' => 2, // base unit price shown to user ex gst should be 2 by default.
'fee_ex_tax_decimals' => 4, // fee ex tax should be 4 decimals.
'fee_inc_tax_decimals' => 4, // fee inc tax should be 4 decimals.
'fee_total_tax_decimals' => 4, // fee total tax should be 4 decimals.
'tax_decimals' => 4, // unit price tax can be 4 decimals. Eg. $2.81 * 1.1 = $3.091 inc tax = $0.281 tax.
'tax_total_decimals' => 4, // total tax can be 4 decimals. Eg. $0.281 * 12 qty = $3.372 total tax.
'subtotal_ex_tax_decimals' => 4, // subtotal ex tax after qty should be 4 decimals by default, then round to show user after qty.
'subtotal_inc_tax_decimals' => 4, // subtotal inc tax after qty should be 4 decimals by default, then round to show user after qty
'total_decimals' => 4, // total after tax should be 4 decimals by default, then round to show user after qty.
// @deprecated
'decimals' => 4,
'decimal_point' => '.',
'thousand_seperator' => ''
],
/*
|--------------------------------------------------------------------------
| Allows you to choose if the discounts applied to fees
|--------------------------------------------------------------------------
|
*/
'discountOnFees' => false,
];

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateShoppingcartTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create(config('cart.database.table'), function (Blueprint $table) {
$table->string('identifier');
$table->string('instance');
$table->longText('content');
$table->nullableTimestamps();
$table->primary(['identifier', 'instance']);
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::drop(config('cart.database.table'));
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -0,0 +1,54 @@
<?php
namespace Gloudemans\Shoppingcart;
trait CanBeBought
{
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null)
{
return method_exists($this, 'getKey') ?
$this->getKey() :
$this->id;
}
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null)
{
if (property_exists($this, 'name')) {
return $this->name;
}
if (property_exists($this, 'title')) {
return $this->title;
}
if (property_exists($this, 'description')) {
return $this->description;
}
return null;
}
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null)
{
if (property_exists($this, 'price')) {
return $this->price;
}
return null;
}
}

View file

@ -0,0 +1,842 @@
<?php
namespace Gloudemans\Shoppingcart;
use Closure;
use DateTime;
use Gloudemans\Shoppingcart\Traits\CartHelper;
use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;
use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Events\Dispatcher;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Gloudemans\Shoppingcart\Exceptions\UnknownModelException;
use Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException;
use Gloudemans\Shoppingcart\Exceptions\CartAlreadyStoredException;
class Cart
{
use CartHelper;
const DEFAULT_INSTANCE = 'default';
public $items;
public $fees;
/**
* Instance of the session manager.
*
* @var \Illuminate\Session\SessionManager
*/
protected $session;
/**
* Instance of the event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $events;
/**
* Holds the current cart instance.
*
* @var string
*/
private $instance;
/**
* Cart constructor.
*
* @param \Illuminate\Session\SessionManager $session
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function __construct(SessionManager $session, Dispatcher $events)
{
$this->items = new Collection;
$this->fees = new Collection;
$this->session = $session;
$this->events = $events;
$this->instance(self::DEFAULT_INSTANCE);
}
/**
* Set the current cart instance.
*
* @param string|null $instance
* @return \Gloudemans\Shoppingcart\Cart
*/
public function instance($instance = null)
{
$instance = $instance ?: self::DEFAULT_INSTANCE;
$this->instance = sprintf('%s.%s', 'cart', $instance);
return $this;
}
/**
* Get the current cart instance.
*
* @return string
*/
public function currentInstance()
{
return str_replace('cart.', '', $this->instance);
}
/**
* Add an item to the cart.
*
* @param $id
* @param $name
* @param $qty
* @param $price
* @param $taxRate
* @param $taxIncluded
* @param array $options
* @param array $eventOptions
* @return array|array[]|CartItem|CartItem[]
*/
public function add($id, $name = null, $qty = null, $price = null, $taxRate = null, $taxIncluded = false, array $options = [], array $eventOptions = [])
{
if ($this->isMulti($id)) {
return array_map(function ($item) {
return $this->add($item);
}, $id);
}
if ($id instanceof CartItem) {
$cartItem = $id;
} else {
$cartItem = $this->createCartItem($id, $name, $qty, $price, $taxRate, $taxIncluded, $options);
}
$content = $this->getContent();
if ($content->has($cartItem->rowId)) {
$cartItem->qty += $content->get($cartItem->rowId)->qty;
}
$content->put($cartItem->rowId, $cartItem);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.added', [
$eventOptions,
]);
return $cartItem;
}
/**
* Update the cart item with the given rowId.
*
* @param $rowId
* @param $qty
* @param array $eventOptions
* @return CartItem|void
*/
public function update($rowId, $qty, array $eventOptions = [])
{
$cartItem = $this->get($rowId);
if ($qty instanceof Buyable) {
$cartItem->updateFromBuyable($qty);
} elseif (is_array($qty)) {
$cartItem->updateFromArray($qty);
} else {
$cartItem->qty = $qty;
}
$content = $this->getContent();
if ($rowId !== $cartItem->rowId) {
if ($content->has($cartItem->rowId)) {
$existingCartItem = $this->get($cartItem->rowId);
$cartItem->setQuantity($existingCartItem->qty + $cartItem->qty);
}
$content = $content->mapWithKeys(function ($val, $key) use ($rowId, $cartItem) {
if ($key === $rowId) {
return [ $cartItem->rowId => $cartItem ];
}
return [ $key => $val ];
});
$this->items = $content;
}
if ($cartItem->qty <= 0) {
$this->remove($cartItem->rowId);
return;
}
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.updated', [
$eventOptions
]);
return $cartItem;
}
/**
* Remove the cart item with the given rowId from the cart.
*
* @param $rowId
* @param array $eventOptions
* @return void
*/
public function remove($rowId, array $eventOptions = [])
{
$cartItem = $this->get($rowId);
$content = $this->getContent();
$content->pull($cartItem->rowId);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.removed', [
$eventOptions
]);
}
/**
* Get a cart item from the cart by its rowId.
*
* @param string $rowId
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function get($rowId)
{
$content = $this->getContent();
if ($content->has($rowId) === false) {
throw new InvalidRowIDException("The cart does not contain rowId {$rowId}.");
}
return $content->get($rowId);
}
/**
* Destroy the current cart instance.
*
* @return void
*/
public function destroy()
{
$this->session->remove($this->instance);
}
/**
* Get the content of the cart.
*
* @return \Illuminate\Support\Collection
*/
public function content()
{
return $this->getContent();
}
/**
* Get the number of items in the cart.
*
* @return int|float
*/
public function count()
{
$content = $this->getContent();
return $content->sum('qty');
}
/**
* Get the total price of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->total);
}, 0);
if ($withFees === true) {
$fees = $this->feeTotal(null, null, null, true);
$total = $total + $fees;
}
$decimals = is_null($decimals) ? config('cart.format.total_decimals') : $decimals;
return $this->numberFormat($total, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$tax = $content->reduce(function ($tax, CartItem $cartItem) {
return $tax + ($cartItem->taxTotal);
}, 0);
if ($withFees === true) {
$fees = $this->feeTax();
$tax = $tax + floatval($fees);
}
$decimals = is_null($decimals) ? config('cart.format.tax_decimals') : $decimals;
return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function feeTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$tax = 0;
foreach ($this->getFees() as $fee) {
$tax += $fee->tax;
}
$decimals = is_null($decimals) ? config('cart.format.fee_total_tax_decimals') : $decimals;
return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the subtotal (ex tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
return $subTotal + ($cartItem->subtotal);
}, 0);
$decimals = is_null($decimals) ? config('cart.format.subtotal_ex_tax_decimals') : $decimals;
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the subtotal (total - tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function subtotalTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
return $subTotal + ($cartItem->subtotalTax);
}, 0);
$decimals = is_null($decimals) ? config('cart.format.subtotal_inc_tax_decimals') : $decimals;
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Search the cart content for a cart item matching the given search closure.
*
* @param Closure $search
* @return \Illuminate\Support\Collection
*/
public function search(Closure $search)
{
$content = $this->getContent();
return $content->filter($search);
}
/**
* Associate the cart item with the given rowId with the given model.
*
* @param string $rowId
* @param mixed $model
* @return void
*/
public function associate($rowId, $model)
{
if (
is_string($model) === true &&
class_exists($model) === false
) {
throw new UnknownModelException("The supplied model {$model} does not exist.");
}
$cartItem = $this->get($rowId);
$cartItem->associate($model);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->instance, $this->toArray());
}
/**
* Set the tax rate for the cart item with the given rowId.
*
* @param string $rowId
* @param int|float $taxRate
* @return void
*/
public function setTax($rowId, $taxRate)
{
$cartItem = $this->get($rowId);
$cartItem->setTaxRate($taxRate);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
}
/**
* Store an the current instance of the cart.
*
* @param $identifier
* @param array $eventOptions
* @return void
*/
public function store($identifier, array $eventOptions = [])
{
// Remove any existing identifiers
// Although possibly first or update could work in future
$this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->delete();
// Insert into the database with the new cart
$content = $this->getContent();
//dd($content);
$this
->getConnection()
->table($this->getTableName())
->insert([
'identifier' => $identifier,
'instance' => $this->currentInstance(),
'content' => serialize($content),
'created_at'=> new DateTime(),
]);
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
], $eventOptions);
$this->events->dispatch('cart.stored', [
$eventOptions,
]);
}
/**
* Restore the cart with the given identifier.
*
* @param $identifier
* @param array $eventOptions
* @return void
*/
public function restore($identifier, array $eventOptions = [], $delteStoredCart = true)
{
if ($this->storedCartWithIdentifierExists($identifier) === false) {
return;
}
// Find any existing carts by identifier
$stored = $this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->first();
// Unserialize the content (either array if new, or collection if old)
$storedContent = unserialize($stored->content);
$currentInstance = $this->currentInstance();
$this->instance($stored->instance);
$content = $this->getContent();
// If the new approach and is array, set this class up.
// Note that it overrides any existing items in cart
// Does not add to existing.
if (is_array($storedContent)) {
$this->fromArray($storedContent);
}
// If the old approach and is Collection, push into existing items
if ($storedContent instanceof Collection) {
foreach ($storedContent as $cartItem) {
$content->put($cartItem->rowId, $cartItem);
}
}
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
], $eventOptions);
$this->events->dispatch('cart.restored', [
$eventOptions,
]);
$this->session->put($this->instance, $content);
$this->instance($currentInstance);
if($delteStoredCart === true) {
$this->deleteStoredCart($identifier);
}
}
/**
* Gets a specific fee from the fees array.
*
* @param $name
*
* @return mixed
*/
public function getFee($name)
{
return $this->fees->get($name, new CartFee(null, null));
}
/**
* Allows to charge for additional fees that may or may not be taxable
* ex - service fee , delivery fee, tips.
*
* Because it uses ->put, the name must be unique otherwise will be overwritten.
*
* @param $name
* @param $amount
* @param $taxRate
* @param array $options
*/
public function addFee($name, $amount, $taxRate = null, array $options = [])
{
$this->fees->put($name, new CartFee($amount, $taxRate, $options));
$this->session->put($this->instance, $this->toArray());
}
/**
* Removes a fee from the fee array.
*
* @todo test to see if i need to restore this
*
* @param $name
*/
public function removeFee($name)
{
$this->fees->forget($name);
$this->session->put($this->instance, $this->toArray());
}
/**
* Removes all the fees set in the cart.
*/
public function removeFees()
{
$this->fees = new Collection;
$this->session->put($this->instance, $this->toArray());
}
/**
* Gets all the fee totals.
*
* @param bool $format
* @param bool $withTax
*
* @return string
*/
public function feeTotal($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withTax = true)
{
$feeTotal = 0;
foreach ($this->getFees() as $fee) {
$feeTotal += $fee->amount;
if ($withTax === true && $fee->taxRate > 0) {
$feeTotal += $fee->tax;
}
}
return $this->numberFormat($feeTotal, null, null, null);
}
/**
* Gets all the fees on the cart object.
*
* @return mixed
*/
public function getFees()
{
return $this->fees;
}
/**
* Magic method to make accessing the total, tax and subtotal properties possible.
*
* @param string $attribute
* @return float|null
*/
public function __get($attribute)
{
if ($attribute === 'total') {
return $this->total();
}
if ($attribute === 'feeTotal') {
return $this->feeTotal(null, null, null, false);
}
if ($attribute === 'feeTotalTax') {
return $this->feeTotal(null, null, null, true);
}
if ($attribute === 'tax') {
return $this->tax();
}
if ($attribute === 'feeTax') {
return $this->feeTax();
}
if ($attribute === 'subtotal') {
return $this->subtotal();
}
if ($attribute === 'subtotalTax') {
return $this->subtotalTax();
}
return null;
}
/**
* @return array
*/
public function toArray()
{
return [
'items' => $this->items,
'fees' => $this->fees,
];
}
/**
* @param $array
* @return $this
*/
public function fromArray($array)
{
$this->items = $array['items'];
$this->fees = $array['fees'];
return $this;
}
/**
* Deletes the stored cart with given identifier
*
* @param mixed $identifier
*/
public function deleteStoredCart($identifier)
{
$this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->delete();
}
/**
* Get the carts content, if there is no cart content set yet, return a new empty Collection
*
* @return \Illuminate\Support\Collection
*/
public function getContent(): Collection
{
$instanceExists = $this->session->has($this->instance);
if ($instanceExists === false) {
$this->items = new Collection;
return $this->items;
}
$instance = $this->session->get($this->instance);
// dd($instance);
// If new approach, set $this variables
if (is_array($instance) === true) {
$this->items = $instance['items'];
$this->fees = $instance['fees'];
}
if ($instance instanceof Collection) {
$this->items = $instance;
}
return $this->items;
}
/**
* Create a new CartItem from the supplied attributes.
*
* @param mixed $id
* @param mixed $name
* @param int|float $qty
* @param float $price
* @param array $options
* @return \Gloudemans\Shoppingcart\CartItem
*/
private function createCartItem($id, $name, $qty, $price, $taxRate, bool $taxIncluded, array $options): CartItem
{
if ($id instanceof Buyable) {
$cartItem = CartItem::fromBuyable($id, $qty ?: []);
$cartItem->setQuantity($name ?: 1);
$cartItem->associate($id);
} elseif (is_array($id)) {
$cartItem = CartItem::fromArray($id);
$cartItem->setQuantity($id['qty']);
} else {
$cartItem = CartItem::fromAttributes($id, $name, $price, $options);
$cartItem->setQuantity($qty);
}
$taxRate = is_int($taxRate) ? $taxRate : config('cart.tax');
$cartItem->setTaxRate($taxRate);
$cartItem->setTaxIncluded($taxIncluded);
return $cartItem;
}
/**
* Check if the item is a multidimensional array or an array of Buyables.
*
* @param mixed $item
* @return bool
*/
private function isMulti($item): bool
{
if (is_array($item) === false) {
return false;
}
return is_array(head($item)) || head($item) instanceof Buyable === true;
}
/**
* @param $identifier
* @return bool
*/
protected function storedCartWithIdentifierExists($identifier): bool
{
return $this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->exists();
}
/**
* Get the database connection.
*
* @return \Illuminate\Database\Connection
*/
protected function getConnection()
{
$connectionName = $this->getConnectionName();
return app(DatabaseManager::class)->connection($connectionName);
}
/**
* Get the database table name.
*
* @return string
*/
protected function getTableName()
{
return config('cart.database.table', 'shoppingcart');
}
/**
* Get the database connection name.
*
* @return string
*/
private function getConnectionName()
{
$connection = config('cart.database.connection');
return is_null($connection) ? config('database.default') : $connection;
}
}

View file

@ -0,0 +1,118 @@
<?php
namespace Gloudemans\Shoppingcart;
use Gloudemans\Shoppingcart\Traits\CartHelper;
/**
* Class CartFee.
*/
class CartFee
{
use CartHelper;
public $amount;
public $taxRate = 0;
public $options = [];
/**
* CartFee constructor.
*
* @param $amount
* @param $taxRate
* @param array $options
*/
public function __construct($amount, $taxRate = null, array $options = [])
{
$this->amount = floatval($amount);
$this->taxRate = is_null($taxRate) ? config('cart.tax') : $taxRate;
$this->options = new CartFeeOptions($options);
}
/**
* Returns the formatted fee amount without TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function amountWithouTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$decimals = is_null($decimals) ? config('cart.format.fee_ex_tax_decimals') : $decimals;
return $this->numberFormat($this->price, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted fee amount with TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function amountTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$priceTax = $this->amount + $this->tax;
$decimals = is_null($decimals) ? config('cart.format.fee_inc_tax_decimals') : $decimals;
return $this->numberFormat($priceTax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Gets the formatted amount.
*
* @param bool $format
* @param bool $withTax
*
* @return string
*/
public function getAmount($format = true, $withTax = false)
{
$total = $this->amount;
if ($withTax) {
$total += $this->taxRate * $total;
}
return $this->numberFormat($total);
}
/**
* @return string
*/
public function tax()
{
$tax = $this->amount * $this->taxRate / 100;
return $this->numberFormat($tax);
}
/**
* Magic method to make accessing the total, tax and subtotal properties possible.
*
* @param string $attribute
* @return float|null
*/
public function __get($attribute)
{
if (property_exists($this, $attribute)) {
return $this->{$attribute};
}
if ($attribute === 'amountWithouTax') {
return $this->amountWithouTax();
}
if ($attribute === 'amountTax') {
return $this->amountTax();
}
if ($attribute === 'tax') {
return $this->tax();
}
return null;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Gloudemans\Shoppingcart;
use Illuminate\Support\Collection;
class CartFeeOptions extends Collection
{
/**
* Get the option by the given key.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
}

View file

@ -0,0 +1,470 @@
<?php
namespace Gloudemans\Shoppingcart;
use Gloudemans\Shoppingcart\Traits\CartHelper;
use Illuminate\Contracts\Support\Arrayable;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Collection;
class CartItem implements Arrayable, Jsonable
{
use CartHelper;
/**
* The rowID of the cart item.
*
* @var string
*/
public $rowId;
/**
* The ID of the cart item.
*
* @var int|string
*/
public $id;
/**
* The quantity for this cart item.
*
* @var int|float
*/
public $qty;
/**
* The name of the cart item.
*
* @var string
*/
public $name;
/**
* The price without TAX of the cart item.
*
* @var float
*/
public $price;
/**
* The options for this cart item.
*
* @var array
*/
public $options;
/**
* The FQN of the associated model.
*
* @var string|null
*/
private $associatedModel = null;
/**
* The tax rate for the cart item.
*
* @var int|float
*/
private $taxRate = 0;
/**
* Whether the tax is already included.
*
* @var bool
*/
private $taxIncluded = false;
/**
* Is item saved for later.
*
* @var boolean
*/
private $isSaved = false;
/**
* CartItem constructor.
*
* @param int|string $id
* @param string $name
* @param float $price
* @param array $options
*/
public function __construct($id, $name, $price, array $options = [])
{
if(empty($id)) {
throw new \InvalidArgumentException('Please supply a valid identifier.');
}
if(empty($name)) {
throw new \InvalidArgumentException('Please supply a valid name.');
}
if(strlen($price) < 0 || ! is_numeric($price)) {
throw new \InvalidArgumentException('Please supply a valid price.');
}
$this->id = $id;
$this->name = $name;
$this->price = floatval($price);
$this->options = new CartItemOptions($options);
$this->rowId = $this->generateRowId($id, $options);
}
/**
* Returns the formatted price without TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function price($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$decimals = is_null($decimals) ? config('cart.format.price_ex_tax_decimals') : $decimals;
return $this->numberFormat($this->price, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted price with TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function priceTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->taxIncluded === true) {
$priceTax = $this->price;
} else {
$priceTax = $this->price + $this->tax;
}
$decimals = is_null($decimals) ? config('cart.format.price_inc_tax_decimals') : $decimals;
return $this->numberFormat($priceTax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted subtotal.
* Subtotal is price for whole CartItem without TAX
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$subtotal = $this->qty * $this->price;
$decimals = is_null($decimals) ? config('cart.format.subtotal_ex_tax_decimals') : $decimals;
return $this->numberFormat($subtotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted subtotal.
* Subtotal is price for whole CartItem with TAX
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function subtotalTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->taxIncluded === true) {
$subtotal = $this->subtotal;
} else {
$subtotal = $this->subtotal + $this->taxTotal;
}
$decimals = is_null($decimals) ? config('cart.format.subtotal_inc_tax_decimals') : $decimals;
return $this->numberFormat($subtotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted total.
* Total is price for whole CartItem with tax
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$total = $this->subtotalTax;
$decimals = is_null($decimals) ? config('cart.format.total_decimals') : $decimals;
return $this->numberFormat($total, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted tax.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if ($this->taxIncluded === true) {
$tax = $this->price / (1 + $this->taxRate);
} else {
$tax = $this->price * ($this->taxRate / 100);
}
$decimals = is_null($decimals) ? config('cart.format.tax_decimals') : $decimals;
return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Returns the formatted tax.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function taxTotal($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$taxTotal = $this->tax * $this->qty;
$decimals = is_null($decimals) ? config('cart.format.tax_total_decimals') : $decimals;
return $this->numberFormat($taxTotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Set the quantity for this cart item.
*
* @param int|float $qty
*/
public function setQuantity($qty)
{
if(empty($qty) || ! is_numeric($qty))
throw new \InvalidArgumentException('Please supply a valid quantity.');
$this->qty = $qty;
}
/**
* Update the cart item from a Buyable.
*
* @param \Gloudemans\Shoppingcart\Contracts\Buyable $item
* @return void
*/
public function updateFromBuyable(Buyable $item)
{
$this->id = $item->getBuyableIdentifier($this->options);
$this->name = $item->getBuyableDescription($this->options);
$this->price = $item->getBuyablePrice($this->options);
$this->priceTax = $this->price + $this->tax;
}
/**
* Update the cart item from an array.
*
* @param array $attributes
* @return void
*/
public function updateFromArray(array $attributes)
{
$this->id = array_get($attributes, 'id', $this->id);
$this->qty = array_get($attributes, 'qty', $this->qty);
$this->name = array_get($attributes, 'name', $this->name);
$this->price = array_get($attributes, 'price', $this->price);
$this->priceTax = $this->price + $this->tax;
$this->options = new CartItemOptions(array_get($attributes, 'options', $this->options));
$this->rowId = $this->generateRowId($this->id, $this->options->all());
}
/**
* Associate the cart item with the given model.
*
* @param mixed $model
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function associate($model)
{
$this->associatedModel = is_string($model) ? $model : get_class($model);
return $this;
}
/**
* Set the tax rate.
*
* @param int|float $taxRate
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function setTaxRate($taxRate)
{
$this->taxRate = $taxRate;
return $this;
}
/**
* Set the tax rate.
*
* @param bool $taxIncluded
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function setTaxIncluded(bool $taxIncluded)
{
$this->taxIncluded = $taxIncluded;
return $this;
}
/**
* Set saved state.
*
* @param bool $bool
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function setSaved($bool)
{
$this->isSaved = $bool;
return $this;
}
/**
* Get an attribute from the cart item or get the associated model.
*
* @param string $attribute
* @return mixed
*/
public function __get($attribute)
{
if (property_exists($this, $attribute)) {
return $this->{$attribute};
}
if ($attribute === 'priceTax') {
return $this->priceTax();
}
if ($attribute === 'subtotal') {
return $this->subtotal();
}
if ($attribute === 'subtotalTax') {
return $this->subtotalTax();
}
if ($attribute === 'total') {
return $this->total();
}
if ($attribute === 'tax') {
return $this->tax();
}
if ($attribute === 'taxTotal') {
return $this->taxTotal();
}
if ($attribute === 'model' && isset($this->associatedModel)) {
return with(new $this->associatedModel)->find($this->id);
}
return null;
}
/**
* Create a new instance from a Buyable.
*
* @param \Gloudemans\Shoppingcart\Contracts\Buyable $item
* @param array $options
* @return \Gloudemans\Shoppingcart\CartItem
*/
public static function fromBuyable(Buyable $item, array $options = [])
{
return new self($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $options);
}
/**
* Create a new instance from the given array.
*
* @param array $attributes
* @return \Gloudemans\Shoppingcart\CartItem
*/
public static function fromArray(array $attributes)
{
$options = array_get($attributes, 'options', []);
return new self($attributes['id'], $attributes['name'], $attributes['price'], $options);
}
/**
* Create a new instance from the given attributes.
*
* @param int|string $id
* @param string $name
* @param float $price
* @param array $options
* @return \Gloudemans\Shoppingcart\CartItem
*/
public static function fromAttributes($id, $name, $price, array $options = [])
{
return new self($id, $name, $price, $options);
}
/**
* Generate a unique id for the cart item.
*
* @param string $id
* @param array $options
* @return string
*/
protected function generateRowId($id, array $options)
{
ksort($options);
$uniqueString = '';
if (config('cart.allow_multiple_same_id') === true) {
$uniqueString = microtime(true);
}
return md5($id . $uniqueString . serialize($options));
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return [
'rowId' => $this->rowId,
'id' => $this->id,
'name' => $this->name,
'qty' => $this->qty,
'price' => $this->price,
'options' => $this->options->toArray(),
'tax' => $this->tax,
'isSaved' => $this->isSaved,
'subtotal' => $this->subtotal
];
}
/**
* Convert the object to its JSON representation.
*
* @param int $options
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->toArray(), $options);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Gloudemans\Shoppingcart;
use Illuminate\Support\Collection;
class CartItemOptions extends Collection
{
/**
* Get the option by the given key.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Gloudemans\Shoppingcart\Contracts;
interface Buyable
{
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null);
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null);
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null);
}

View file

@ -0,0 +1,7 @@
<?php
namespace Gloudemans\Shoppingcart\Exceptions;
use RuntimeException;
class CartAlreadyStoredException extends RuntimeException {}

View file

@ -0,0 +1,7 @@
<?php
namespace Gloudemans\Shoppingcart\Exceptions;
use RuntimeException;
class InvalidRowIDException extends RuntimeException {}

View file

@ -0,0 +1,7 @@
<?php
namespace Gloudemans\Shoppingcart\Exceptions;
use RuntimeException;
class UnknownModelException extends RuntimeException {}

View file

@ -0,0 +1,16 @@
<?php
namespace Gloudemans\Shoppingcart\Facades;
use Illuminate\Support\Facades\Facade;
class Cart extends Facade {
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cart';
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Gloudemans\Shoppingcart;
use Illuminate\Auth\Events\Logout;
use Illuminate\Session\SessionManager;
use Illuminate\Support\ServiceProvider;
class ShoppingcartServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind('cart', 'Gloudemans\Shoppingcart\Cart');
$config = __DIR__ . '/../config/cart.php';
$this->mergeConfigFrom($config, 'cart');
$this->publishes([__DIR__ . '/../config/cart.php' => config_path('cart.php')], 'config');
$this->app['events']->listen(Logout::class, function () {
if ($this->app['config']->get('cart.destroy_on_logout')) {
$this->app->make(SessionManager::class)->forget('cart');
}
});
if ( ! class_exists('CreateShoppingcartTable')) {
// Publish the migration
$timestamp = date('Y_m_d_His', time());
$this->publishes([
__DIR__.'/../database/migrations/0000_00_00_000000_create_shoppingcart_table.php' => database_path('migrations/'.$timestamp.'_create_shoppingcart_table.php'),
], 'migrations');
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Gloudemans\Shoppingcart\Traits;
/**
* trait CartHelper
*/
trait CartHelper
{
/**
* Get the Formated number
*
* @param $value
* @param $decimals
* @param $decimalPoint
* @param $thousandSeperator
* @return string
*/
private function numberFormat($value, $decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
if (is_null($decimals)) {
$decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals');
}
if (is_null($decimalPoint)) {
$decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point');
}
if (is_null($thousandSeperator)) {
$thousandSeperator = is_null(config('cart.format.thousand_seperator')) ? ',' : config('cart.format.thousand_seperator');
}
return number_format($value, $decimals, $decimalPoint, $thousandSeperator);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Gloudemans\Shoppingcart\Cart;
use PHPUnit\Framework\Assert as PHPUnit;
trait CartAssertions
{
/**
* Assert that the cart contains the given number of items.
*
* @param int|float $items
* @param \Gloudemans\Shoppingcart\Cart $cart
*/
public function assertItemsInCart($items, Cart $cart)
{
$actual = $cart->count();
PHPUnit::assertEquals($items, $cart->count(), "Expected the cart to contain {$items} items, but got {$actual}.");
}
/**
* Assert that the cart contains the given number of rows.
*
* @param int $rows
* @param \Gloudemans\Shoppingcart\Cart $cart
*/
public function assertRowsInCart($rows, Cart $cart)
{
$actual = $cart->content()->count();
PHPUnit::assertCount($rows, $cart->content(), "Expected the cart to contain {$rows} rows, but got {$actual}.");
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Orchestra\Testbench\TestCase;
use Gloudemans\Shoppingcart\CartItem;
use Gloudemans\Shoppingcart\ShoppingcartServiceProvider;
class CartItemTest extends TestCase
{
/**
* Set the package service provider.
*
* @param \Illuminate\Foundation\Application $app
* @return array
*/
protected function getPackageProviders($app)
{
return [ShoppingcartServiceProvider::class];
}
/** @test */
public function it_can_be_cast_to_an_array()
{
$cartItem = new CartItem(1, 'Some item', 10.00, ['size' => 'XL', 'color' => 'red']);
$cartItem->setQuantity(2);
$this->assertEquals([
'id' => 1,
'name' => 'Some item',
'price' => 10.00,
'rowId' => '07d5da5550494c62daf9993cf954303f',
'qty' => 2,
'options' => [
'size' => 'XL',
'color' => 'red'
],
'tax' => 0.0,
'subtotal' => 20.00,
'isSaved' => false
], $cartItem->toArray());
}
/** @test */
public function it_can_be_cast_to_json()
{
$cartItem = new CartItem(1, 'Some item', 10.00, ['size' => 'XL', 'color' => 'red']);
$cartItem->setQuantity(2);
$this->assertJson($cartItem->toJson());
$json = '{"rowId":"07d5da5550494c62daf9993cf954303f","id":1,"name":"Some item","qty":2,"price":10,"options":{"size":"XL","color":"red"},"tax":"0.00","isSaved":false,"subtotal":"20.00"}';
$this->assertEquals($json, $cartItem->toJson());
}
}

View file

@ -0,0 +1,945 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Mockery;
use PHPUnit\Framework\Assert;
use Gloudemans\Shoppingcart\Cart;
use Orchestra\Testbench\TestCase;
use Illuminate\Auth\Events\Logout;
use Illuminate\Support\Collection;
use Gloudemans\Shoppingcart\CartItem;
use Illuminate\Support\Facades\Event;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Gloudemans\Shoppingcart\ShoppingcartServiceProvider;
use Gloudemans\Tests\Shoppingcart\Fixtures\ProductModel;
use Gloudemans\Tests\Shoppingcart\Fixtures\BuyableProduct;
class CartTest extends TestCase
{
use CartAssertions;
/**
* Set the package service provider.
*
* @param \Illuminate\Foundation\Application $app
* @return array
*/
protected function getPackageProviders($app)
{
return [ShoppingcartServiceProvider::class];
}
/**
* Define environment setup.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
protected function getEnvironmentSetUp($app)
{
$app['config']->set('cart.database.connection', 'testing');
$app['config']->set('session.driver', 'array');
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
/**
* Setup the test environment.
*
* @return void
*/
protected function setUp()
{
parent::setUp();
$this->app->afterResolving('migrator', function ($migrator) {
$migrator->path(realpath(__DIR__.'/../database/migrations'));
});
}
/** @test */
public function it_has_a_default_instance()
{
$cart = $this->getCart();
$this->assertEquals(Cart::DEFAULT_INSTANCE, $cart->currentInstance());
}
/** @test */
public function it_can_have_multiple_instances()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item'));
$cart->instance('wishlist')->add(new BuyableProduct(2, 'Second item'));
$this->assertItemsInCart(1, $cart->instance(Cart::DEFAULT_INSTANCE));
$this->assertItemsInCart(1, $cart->instance('wishlist'));
}
/** @test */
public function it_can_add_an_item()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_will_return_the_cartitem_of_the_added_item()
{
Event::fake();
$cart = $this->getCart();
$cartItem = $cart->add(new BuyableProduct);
$this->assertInstanceOf(CartItem::class, $cartItem);
$this->assertEquals('027c91341fd5cf4d2579b49c4b6a90da', $cartItem->rowId);
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_multiple_buyable_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cart->add([new BuyableProduct(1), new BuyableProduct(2)]);
$this->assertEquals(2, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_will_return_an_array_of_cartitems_when_you_add_multiple_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cartItems = $cart->add([new BuyableProduct(1), new BuyableProduct(2)]);
$this->assertTrue(is_array($cartItems));
$this->assertCount(2, $cartItems);
$this->assertContainsOnlyInstancesOf(CartItem::class, $cartItems);
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_from_attributes()
{
Event::fake();
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_from_an_array()
{
Event::fake();
$cart = $this->getCart();
$cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => 10.00]);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_multiple_array_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cart->add([
['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => 10.00],
['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => 10.00]
]);
$this->assertEquals(2, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_with_options()
{
Event::fake();
$cart = $this->getCart();
$options = ['size' => 'XL', 'color' => 'red'];
$cart->add(new BuyableProduct, 1, $options);
$cartItem = $cart->get('07d5da5550494c62daf9993cf954303f');
$this->assertInstanceOf(CartItem::class, $cartItem);
$this->assertEquals('XL', $cartItem->options->size);
$this->assertEquals('red', $cartItem->options->color);
Event::assertDispatched('cart.added');
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid identifier.
*/
public function it_will_validate_the_identifier()
{
$cart = $this->getCart();
$cart->add(null, 'Some title', 1, 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid name.
*/
public function it_will_validate_the_name()
{
$cart = $this->getCart();
$cart->add(1, null, 1, 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid quantity.
*/
public function it_will_validate_the_quantity()
{
$cart = $this->getCart();
$cart->add(1, 'Some title', 'invalid', 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid price.
*/
public function it_will_validate_the_price()
{
$cart = $this->getCart();
$cart->add(1, 'Some title', 1, 'invalid');
}
/** @test */
public function it_will_update_the_cart_if_the_item_already_exists_in_the_cart()
{
$cart = $this->getCart();
$item = new BuyableProduct;
$cart->add($item);
$cart->add($item);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_will_keep_updating_the_quantity_when_an_item_is_added_multiple_times()
{
$cart = $this->getCart();
$item = new BuyableProduct;
$cart->add($item);
$cart->add($item);
$cart->add($item);
$this->assertItemsInCart(3, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_can_update_the_quantity_of_an_existing_item_in_the_cart()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', 2);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
Event::assertDispatched('cart.updated');
}
/** @test */
public function it_can_update_an_existing_item_in_the_cart_from_a_buyable()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', new BuyableProduct(1, 'Different description'));
$this->assertItemsInCart(1, $cart);
$this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name);
Event::assertDispatched('cart.updated');
}
/** @test */
public function it_can_update_an_existing_item_in_the_cart_from_an_array()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', ['name' => 'Different description']);
$this->assertItemsInCart(1, $cart);
$this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name);
Event::assertDispatched('cart.updated');
}
/**
* @test
* @expectedException \Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException
*/
public function it_will_throw_an_exception_if_a_rowid_was_not_found()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('none-existing-rowid', new BuyableProduct(1, 'Different description'));
}
/** @test */
public function it_will_regenerate_the_rowid_if_the_options_changed()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct, 1, ['color' => 'red']);
$cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => ['color' => 'blue']]);
$this->assertItemsInCart(1, $cart);
$this->assertEquals('7e70a1e9aaadd18c72921a07aae5d011', $cart->content()->first()->rowId);
$this->assertEquals('blue', $cart->get('7e70a1e9aaadd18c72921a07aae5d011')->options->color);
}
/** @test */
public function it_will_add_the_item_to_an_existing_row_if_the_options_changed_to_an_existing_rowid()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct, 1, ['color' => 'red']);
$cart->add(new BuyableProduct, 1, ['color' => 'blue']);
$cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => ['color' => 'red']]);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_can_remove_an_item_from_the_cart()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->remove('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_will_remove_the_item_if_its_quantity_was_set_to_zero()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', 0);
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_will_remove_the_item_if_its_quantity_was_set_negative()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', -1);
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_can_get_an_item_from_the_cart_by_its_rowid()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertInstanceOf(CartItem::class, $cartItem);
}
/** @test */
public function it_can_get_the_content_of_the_cart()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1));
$cart->add(new BuyableProduct(2));
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertCount(2, $content);
}
/** @test */
public function it_will_return_an_empty_collection_if_the_cart_is_empty()
{
$cart = $this->getCart();
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertCount(0, $content);
}
/** @test */
public function it_will_include_the_tax_and_subtotal_when_converted_to_an_array()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1));
$cart->add(new BuyableProduct(2));
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertEquals([
'027c91341fd5cf4d2579b49c4b6a90da' => [
'rowId' => '027c91341fd5cf4d2579b49c4b6a90da',
'id' => 1,
'name' => 'Item name',
'qty' => 1,
'price' => 10.00,
'tax' => 2.10,
'subtotal' => 10.0,
'isSaved' => false,
'options' => [],
],
'370d08585360f5c568b18d1f2e4ca1df' => [
'rowId' => '370d08585360f5c568b18d1f2e4ca1df',
'id' => 2,
'name' => 'Item name',
'qty' => 1,
'price' => 10.00,
'tax' => 2.10,
'subtotal' => 10.0,
'isSaved' => false,
'options' => [],
]
], $content->toArray());
}
/** @test */
public function it_can_destroy_a_cart()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$this->assertItemsInCart(1, $cart);
$cart->destroy();
$this->assertItemsInCart(0, $cart);
}
/** @test */
public function it_can_get_the_total_price_of_the_cart_content()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 10.00));
$cart->add(new BuyableProduct(2, 'Second item', 25.00), 2);
$this->assertItemsInCart(3, $cart);
$this->assertEquals(60.00, $cart->subtotal());
}
/** @test */
public function it_can_return_a_formatted_total()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 1000.00));
$cart->add(new BuyableProduct(2, 'Second item', 2500.00), 2);
$this->assertItemsInCart(3, $cart);
$this->assertEquals('6.000,00', $cart->subtotal(2, ',', '.'));
}
/** @test */
public function it_can_search_the_cart_for_a_specific_item()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'));
$cart->add(new BuyableProduct(2, 'Another item'));
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->name == 'Some item';
});
$this->assertInstanceOf(Collection::class, $cartItem);
$this->assertCount(1, $cartItem);
$this->assertInstanceOf(CartItem::class, $cartItem->first());
$this->assertEquals(1, $cartItem->first()->id);
}
/** @test */
public function it_can_search_the_cart_for_multiple_items()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'));
$cart->add(new BuyableProduct(2, 'Some item'));
$cart->add(new BuyableProduct(3, 'Another item'));
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->name == 'Some item';
});
$this->assertInstanceOf(Collection::class, $cartItem);
}
/** @test */
public function it_can_search_the_cart_for_a_specific_item_with_options()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'), 1, ['color' => 'red']);
$cart->add(new BuyableProduct(2, 'Another item'), 1, ['color' => 'blue']);
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->options->color == 'red';
});
$this->assertInstanceOf(Collection::class, $cartItem);
$this->assertCount(1, $cartItem);
$this->assertInstanceOf(CartItem::class, $cartItem->first());
$this->assertEquals(1, $cartItem->first()->id);
}
/** @test */
public function it_will_associate_the_cart_item_with_a_model_when_you_add_a_buyable()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertContains(BuyableProduct::class, Assert::readAttribute($cartItem, 'associatedModel'));
}
/** @test */
public function it_can_associate_the_cart_item_with_a_model()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(ProductModel::class, Assert::readAttribute($cartItem, 'associatedModel'));
}
/**
* @test
* @expectedException \Gloudemans\Shoppingcart\Exceptions\UnknownModelException
* @expectedExceptionMessage The supplied model SomeModel does not exist.
*/
public function it_will_throw_an_exception_when_a_non_existing_model_is_being_associated()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', 'SomeModel');
}
/** @test */
public function it_can_get_the_associated_model_of_a_cart_item()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertInstanceOf(ProductModel::class, $cartItem->model);
$this->assertEquals('Some value', $cartItem->model->someValue);
}
/** @test */
public function it_can_calculate_the_subtotal_of_a_cart_item()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 9.99), 3);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(29.97, $cartItem->subtotal);
}
/** @test */
public function it_can_return_a_formatted_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 500), 3);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('1.500,00', $cartItem->subtotal(2, ',', '.'));
}
/** @test */
public function it_can_calculate_tax_based_on_the_default_tax_rate_in_the_config()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(2.10, $cartItem->tax);
}
/** @test */
public function it_can_calculate_tax_based_on_the_specified_tax()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(1.90, $cartItem->tax);
}
/** @test */
public function it_can_return_the_calculated_tax_formatted()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10000.00), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('2.100,00', $cartItem->tax(2, ',', '.'));
}
/** @test */
public function it_can_calculate_the_total_tax_for_all_cart_items()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 20.00), 2);
$this->assertEquals(10.50, $cart->tax);
}
/** @test */
public function it_can_return_formatted_total_tax()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('1.050,00', $cart->tax(2, ',', '.'));
}
/** @test */
public function it_can_return_the_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 20.00), 2);
$this->assertEquals(50.00, $cart->subtotal);
}
/** @test */
public function it_can_return_formatted_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('5000,00', $cart->subtotal(2, ',', ''));
}
/** @test */
public function it_can_return_cart_formated_numbers_by_config_values()
{
$this->setConfigFormat(2, ',', '');
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('5000,00', $cart->subtotal());
$this->assertEquals('1050,00', $cart->tax());
$this->assertEquals('6050,00', $cart->total());
$this->assertEquals('5000,00', $cart->subtotal);
$this->assertEquals('1050,00', $cart->tax);
$this->assertEquals('6050,00', $cart->total);
}
/** @test */
public function it_can_return_cartItem_formated_numbers_by_config_values()
{
$this->setConfigFormat(2, ',', '');
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 2000.00), 2);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('2000,00', $cartItem->price());
$this->assertEquals('2420,00', $cartItem->priceTax());
$this->assertEquals('4000,00', $cartItem->subtotal());
$this->assertEquals('4840,00', $cartItem->total());
$this->assertEquals('420,00', $cartItem->tax());
$this->assertEquals('840,00', $cartItem->taxTotal());
}
/** @test */
public function it_can_store_the_cart_in_a_database()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$serialized = serialize($cart->content());
$this->assertDatabaseHas('shoppingcart', ['identifier' => $identifier, 'instance' => 'default', 'content' => $serialized]);
Event::assertDispatched('cart.stored');
}
/** @test */
public function it_can_update_the_cart_in_database()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$serialized = serialize($cart->content());
$this->assertDatabaseHas('shoppingcart', ['identifier' => $identifier, 'instance' => 'default', 'content' => $serialized]);
Event::assertDispatched('cart.stored');
}
/** @test */
public function it_can_restore_a_cart_from_the_database()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$cart->destroy();
$this->assertItemsInCart(0, $cart);
$cart->restore($identifier);
$this->assertItemsInCart(1, $cart);
Event::assertDispatched('cart.restored');
}
/** @test */
public function it_will_just_keep_the_current_instance_if_no_cart_with_the_given_identifier_was_stored()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
$cart = $this->getCart();
$cart->restore($identifier = 123);
$this->assertItemsInCart(0, $cart);
}
/** @test */
public function it_can_calculate_all_values()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 10.00), 2);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19);
$this->assertEquals(10.00, $cartItem->price(2));
$this->assertEquals(11.90, $cartItem->priceTax(2));
$this->assertEquals(20.00, $cartItem->subtotal(2));
$this->assertEquals(23.80, $cartItem->total(2));
$this->assertEquals(1.90, $cartItem->tax(2));
$this->assertEquals(3.80, $cartItem->taxTotal(2));
$this->assertEquals(20.00, $cart->subtotal(2));
$this->assertEquals(23.80, $cart->total(2));
$this->assertEquals(3.80, $cart->tax(2));
}
/** @test */
public function it_will_destroy_the_cart_when_the_user_logs_out_and_the_config_setting_was_set_to_true()
{
$this->app['config']->set('cart.destroy_on_logout', true);
$this->app->instance(SessionManager::class, Mockery::mock(SessionManager::class, function ($mock) {
$mock->shouldReceive('forget')->once()->with('cart');
}));
$user = Mockery::mock(Authenticatable::class);
$guard = $this->app->make('auth');
event(new Logout($guard, $user));
}
/**
* Get an instance of the cart.
*
* @return \Gloudemans\Shoppingcart\Cart
*/
private function getCart()
{
$session = $this->app->make('session');
$events = $this->app->make('events');
return new Cart($session, $events);
}
/**
* Set the config number format.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
*/
private function setConfigFormat($decimals, $decimalPoint, $thousandSeperator)
{
$this->app['config']->set('cart.format.decimals', $decimals);
$this->app['config']->set('cart.format.decimal_point', $decimalPoint);
$this->app['config']->set('cart.format.thousand_seperator', $thousandSeperator);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Gloudemans\Tests\Shoppingcart\Fixtures;
use Gloudemans\Shoppingcart\Contracts\Buyable;
class BuyableProduct implements Buyable
{
/**
* @var int|string
*/
private $id;
/**
* @var string
*/
private $name;
/**
* @var float
*/
private $price;
/**
* BuyableProduct constructor.
*
* @param int|string $id
* @param string $name
* @param float $price
*/
public function __construct($id = 1, $name = 'Item name', $price = 10.00)
{
$this->id = $id;
$this->name = $name;
$this->price = $price;
}
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null)
{
return $this->id;
}
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null)
{
return $this->name;
}
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null)
{
return $this->price;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Gloudemans\Tests\Shoppingcart\Fixtures;
class ProductModel
{
public $someValue = 'Some value';
public function find($id)
{
return $this;
}
}

5
packages/shoppingcart/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/vendor
composer.phar
composer.lock
.DS_Store
.idea/

View file

@ -0,0 +1,10 @@
language: php
php:
- 7.0
before_script:
- composer self-update
- composer install --prefer-source --no-interaction
script: vendor/bin/phpunit

View file

@ -0,0 +1,43 @@
<?php
namespace App\Packages\Shoppingcart;
trait CanBeBought
{
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null)
{
return method_exists($this, 'getKey') ? $this->getKey() : $this->id;
}
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null)
{
if(property_exists($this, 'name')) return $this->name;
if(property_exists($this, 'title')) return $this->title;
if(property_exists($this, 'description')) return $this->description;
return null;
}
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null)
{
if(property_exists($this, 'price')) return $this->price;
return null;
}
}

View file

@ -0,0 +1,686 @@
<?php
namespace App\Packages\Shoppingcart;
use Carbon\Carbon;
use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;
use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Events\Dispatcher;
use App\Packages\Shoppingcart\Contracts\Buyable;
use App\Packages\Shoppingcart\Exceptions\UnknownModelException;
use App\Packages\Shoppingcart\Exceptions\InvalidRowIDException;
use App\Packages\Shoppingcart\Exceptions\CartAlreadyStoredException;
class Cart
{
const DEFAULT_INSTANCE = 'default';
const COST_SHIPPING = 'shipping';
const COST_TRANSACTION = 'transaction';
/**
* Instance of the session manager.
*
* @var SessionManager
*/
private $session;
/**
* Instance of the event dispatcher.
*
* @var Dispatcher
*/
private $events;
/**
* Holds the current cart instance.
*
* @var string
*/
private $instance;
/**
* Holds the extra additional costs on the cart
*
* @var Collection
*/
private $extraCosts;
/**
* Cart constructor.
*
* @param SessionManager $session
* @param Dispatcher $events
*/
public function __construct(SessionManager $session, Dispatcher $events)
{
$this->session = $session;
$this->events = $events;
$this->extraCosts = new Collection();
$this->instance(self::DEFAULT_INSTANCE);
}
/**
* Set the current cart instance.
*
* @param string|null $instance
* @return Cart
*/
public function instance($instance = null)
{
$instance = $instance ?: self::DEFAULT_INSTANCE;
$this->instance = sprintf('%s.%s', 'cart', $instance);
return $this;
}
/**
* Get the current cart instance.
*
* @return string
*/
public function currentInstance()
{
return str_replace('cart.', '', $this->instance);
}
/**
* Add an item to the cart.
*
* @param mixed $id
* @param mixed $name
* @param int|float $qty
* @param float $price
* @param array $options
* @return CartItem
*/
public function add($id, $name = null, $qty = null, $price = null, array $options = [])
{
if ($this->isMulti($id)) {
return array_map(function ($item) {
return $this->add($item);
}, $id);
}
$cartItem = $this->createCartItem($id, $name, $qty, $price, $options);
$content = $this->getContent();
if ($content->has($cartItem->rowId)) {
$cartItem->qty += $content->get($cartItem->rowId)->qty;
}
$content->put($cartItem->rowId, $cartItem);
$this->events->dispatch('cart.added', $cartItem);
$this->session->put($this->instance, $content);
return $cartItem;
}
/**
* Sets/adds an additional cost on the cart.
*
* @param string $name
* @param float $price
* @todo add in session
*/
public function addCost($name, $price)
{
$oldCost = $this->extraCosts->pull($name, 0);
$this->extraCosts->put($name, $price + $oldCost);
}
/**
* Gets an additional cost by name
*
* @param $name
* @param int|null $decimals
* @param string|null $decimalPoint
* @param string|null $thousandSeparator
* @return string
*/
public function getCost($name)
{
$cost = $this->extraCosts->get($name, 0);
return $this->numberFormat($cost);
}
/**
* Update the cart item with the given rowId.
*
* @param string $rowId
* @param mixed $qty
* @return CartItem
*/
public function update($rowId, $qty)
{
$cartItem = $this->get($rowId);
if ($qty instanceof Buyable) {
$cartItem->updateFromBuyable($qty);
} elseif (is_array($qty)) {
$cartItem->updateFromArray($qty);
} else {
$cartItem->qty = $qty;
}
$content = $this->getContent();
if ($rowId !== $cartItem->rowId) {
$content->pull($rowId);
if ($content->has($cartItem->rowId)) {
$existingCartItem = $this->get($cartItem->rowId);
$cartItem->setQuantity($existingCartItem->qty + $cartItem->qty);
}
}
if ($cartItem->qty <= 0) {
$this->remove($cartItem->rowId);
return;
} else {
$content->put($cartItem->rowId, $cartItem);
}
$this->events->dispatch('cart.updated', $cartItem);
$this->session->put($this->instance, $content);
return $cartItem;
}
/**
* Remove the cart item with the given rowId from the cart.
*
* @param string $rowId
* @return void
*/
public function remove($rowId)
{
$cartItem = $this->get($rowId);
$content = $this->getContent();
$content->pull($cartItem->rowId);
$this->events->dispatch('cart.removed', $cartItem);
$this->session->put($this->instance, $content);
}
/**
* Get a cart item from the cart by its rowId.
*
* @param string $rowId
* @return CartItem
*/
public function get($rowId)
{
$content = $this->getContent();
if ( ! $content->has($rowId))
throw new InvalidRowIDException("The cart does not contain rowId {$rowId}.");
return $content->get($rowId);
}
/**
* Destroy the current cart instance.
*
* @return void
*/
public function destroy()
{
$this->session->remove($this->instance);
}
/**
* Get the content of the cart.
*
* @return Collection
*/
public function content()
{
if (is_null($this->session->get($this->instance))) {
return new Collection();
}
return $this->session->get($this->instance);
}
/**
* Get the number of items in the cart.
*
* @return int|float
*/
public function count()
{
$content = $this->getContent();
return $content->sum('qty');
}
/**
* Get the total price of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function total()
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->qty * $cartItem->priceTax);
}, 0);
$totalCost = $this->extraCosts->reduce(function ($total, $cost) {
return $total + $cost;
}, 0);
$total += $totalCost;
return $this->numberFormat($total);
}
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return float
*/
public function tax()
{
$content = $this->getContent();
$tax = $content->reduce(function ($tax, CartItem $cartItem) {
return $tax + ($cartItem->qty * $cartItem->tax);
}, 0);
return $this->numberFormat($tax);
}
/**
* Get the subtotal (total - tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return float
*/
public function subtotal()
{
$content = $this->getContent();
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
return $subTotal + ($cartItem->qty * $cartItem->price);
}, 0);
return $this->numberFormat($subTotal);
}
/**
* Search the cart content for a cart item matching the given search closure.
*
* @param \Closure $search
* @return Collection
*/
public function search(Closure $search)
{
$content = $this->getContent();
return $content->filter($search);
}
/**
* Associate the cart item with the given rowId with the given model.
*
* @param string $rowId
* @param mixed $model
* @return void
*/
public function associate($rowId, $model)
{
if(is_string($model) && ! class_exists($model)) {
throw new UnknownModelException("The supplied model {$model} does not exist.");
}
$cartItem = $this->get($rowId);
$cartItem->associate($model);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->instance, $content);
}
/**
* Set the tax rate for the cart item with the given rowId.
*
* @param string $rowId
* @param int|float $taxRate
* @return void
*/
public function setTax($rowId, $taxRate)
{
$cartItem = $this->get($rowId);
$cartItem->setTaxRate($taxRate);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->instance, $content);
}
/**
* Store an the current instance of the cart.
*
* @param mixed $identifier
* @return void
*/
public function store($identifier)
{
$content = $this->getContent();
if ($identifier instanceof InstanceIdentifier) {
$identifier = $identifier->getInstanceIdentifier();
}
$instance = $this->currentInstance();
if ($this->storedCartInstanceWithIdentifierExists($instance, $identifier)) {
throw new CartAlreadyStoredException("A cart with identifier {$identifier} was already stored.");
}
$this->getConnection()->table($this->getTableName())->insert([
'identifier' => $identifier,
'instance' => $instance,
'content' => serialize($content),
'created_at' => $this->createdAt ?: Carbon::now(),
'updated_at' => Carbon::now(),
]);
$this->events->dispatch('cart.stored');
}
/**
* @param $identifier
*
* @return bool
*/
private function storedCartInstanceWithIdentifierExists($instance, $identifier)
{
return $this->getConnection()->table($this->getTableName())->where(['identifier' => $identifier, 'instance'=> $instance])->exists();
}
/**
* Restore the cart with the given identifier.
*
* @param mixed $identifier
* @return void
*/
public function restore($identifier)
{
if ($identifier instanceof InstanceIdentifier) {
$identifier = $identifier->getInstanceIdentifier();
}
$currentInstance = $this->currentInstance();
if (!$this->storedCartInstanceWithIdentifierExists($currentInstance, $identifier)) {
return;
}
$stored = $this->getConnection()->table($this->getTableName())
->where(['identifier'=> $identifier, 'instance' => $currentInstance])->first();
$storedContent = unserialize(data_get($stored, 'content'));
$this->instance(data_get($stored, 'instance'));
$content = $this->getContent();
foreach ($storedContent as $cartItem) {
$content->put($cartItem->rowId, $cartItem);
}
$this->events->dispatch('cart.restored');
$this->session->put($this->instance, $content);
$this->instance($currentInstance);
$this->createdAt = Carbon::parse(data_get($stored, 'created_at'));
$this->updatedAt = Carbon::parse(data_get($stored, 'updated_at'));
$this->getConnection()->table($this->getTableName())->where(['identifier' => $identifier, 'instance' => $currentInstance])->delete();
}
/**
* Merges the contents of another cart into this cart.
*
* @param mixed $identifier Identifier of the Cart to merge with.
* @param bool $keepDiscount Keep the discount of the CartItems.
* @param bool $keepTax Keep the tax of the CartItems.
* @param bool $dispatchAdd Flag to dispatch the add events.
*
* @return bool
*/
public function merge($identifier, $keepTax = false, $dispatchAdd = true, $instance = self::DEFAULT_INSTANCE)
{
if (!$this->storedCartInstanceWithIdentifierExists($instance, $identifier)) {
return false;
}
$stored = $this->getConnection()->table($this->getTableName())
->where(['identifier'=> $identifier, 'instance'=> $instance])->first();
$storedContent = unserialize($stored->content);
foreach ($storedContent as $cartItem) {
$this->addCartItem($cartItem, $keepTax, $dispatchAdd);
}
$this->events->dispatch('cart.merged');
return true;
}
/**
* Add an item to the cart.
*
* @param \Gloudemans\Shoppingcart\CartItem $item Item to add to the Cart
* @param bool $keepDiscount Keep the discount rate of the Item
* @param bool $keepTax Keep the Tax rate of the Item
* @param bool $dispatchEvent
*
* @return \Gloudemans\Shoppingcart\CartItem The CartItem
*/
public function addCartItem($item, $keepTax = false, $dispatchEvent = true)
{
if (!$keepTax) {
$item->setTaxRate($this->taxRate);
}
$content = $this->getContent();
if ($content->has($item->rowId)) {
$item->qty += $content->get($item->rowId)->qty;
}
$content->put($item->rowId, $item);
if ($dispatchEvent) {
$this->events->dispatch('cart.adding', $item);
}
$this->session->put($this->instance, $content);
if ($dispatchEvent) {
$this->events->dispatch('cart.added', $item);
}
return $item;
}
/**
* Magic method to make accessing the total, tax and subtotal properties possible.
*
* @param string $attribute
* @return float|null
*/
public function __get($attribute)
{
if($attribute === 'total') {
return $this->total();
}
if($attribute === 'tax') {
return $this->tax();
}
if($attribute === 'subtotal') {
return $this->subtotal();
}
return null;
}
/**
* Get the carts content, if there is no cart content set yet, return a new empty Collection
*
* @return Collection
*/
protected function getContent()
{
$content = $this->session->has($this->instance)
? $this->session->get($this->instance)
: new Collection;
return $content;
}
/**
* Create a new CartItem from the supplied attributes.
*
* @param mixed $id
* @param mixed $name
* @param int|float $qty
* @param float $price
* @param array $options
* @return CartItem
*/
private function createCartItem($id, $name, $qty, $price, array $options)
{
if ($id instanceof Buyable) {
$cartItem = CartItem::fromBuyable($id, $qty ?: []);
$cartItem->setQuantity($name ?: 1);
$cartItem->associate($id);
} elseif (is_array($id)) {
$cartItem = CartItem::fromArray($id);
$cartItem->setQuantity($id['qty']);
} else {
$cartItem = CartItem::fromAttributes($id, $name, $price, $options);
$cartItem->setQuantity($qty);
}
$cartItem->setTaxRate(config('cart.tax'));
return $cartItem;
}
/**
* Check if the item is a multidimensional array or an array of Buyables.
*
* @param mixed $item
* @return bool
*/
private function isMulti($item)
{
if ( ! is_array($item)) return false;
return is_array(head($item)) || head($item) instanceof Buyable;
}
/**
* @param $identifier
* @return bool
*/
private function storedCartWithIdentifierExists($identifier)
{
return $this->getConnection()->table($this->getTableName())->where('identifier', $identifier)->exists();
}
/**
* Get the database connection.
*
* @return Connection
*/
private function getConnection()
{
$connectionName = $this->getConnectionName();
return app(DatabaseManager::class)->connection($connectionName);
}
/**
* Get the database table name.
*
* @return string
*/
private function getTableName()
{
return 'shopping_cart';
}
/**
* Get the database connection name.
*
* @return string
*/
private function getConnectionName()
{
$connection = config('cart.database.connection');
return is_null($connection) ? config('database.default') : $connection;
}
/**
* Get the Formated number
*
* @param $value
* @param $decimals
* @param $decimalPoint
* @param $thousandSeparator
* @return string
*/
private function numberFormat($value)
{
$decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals');
$decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point');
$thousandSeparator = '';
return number_format($value, $decimals, $decimalPoint, $thousandSeparator);
}
}

View file

@ -0,0 +1,386 @@
<?php
namespace App\Packages\Shoppingcart;
use Illuminate\Contracts\Support\Arrayable;
use App\Packages\Contracts\Buyable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Arr;
class CartItem implements Arrayable, Jsonable
{
/**
* The rowID of the cart item.
*
* @var string
*/
public $rowId;
/**
* The ID of the cart item.
*
* @var int|string
*/
public $id;
/**
* The quantity for this cart item.
*
* @var int|float
*/
public $qty;
/**
* The name of the cart item.
*
* @var string
*/
public $name;
/**
* The price without TAX of the cart item.
*
* @var float
*/
public $price;
/**
* The options for this cart item.
*
* @var array
*/
public $options;
/**
* The FQN of the associated model.
*
* @var string|null
*/
private $associatedModel = null;
/**
* The tax rate for the cart item.
*
* @var int|float
*/
private $taxRate = 0;
/**
* CartItem constructor.
*
* @param int|string $id
* @param string $name
* @param float $price
* @param array $options
*/
public function __construct($id, $name, $price, array $options = [])
{
if(empty($id)) {
throw new \InvalidArgumentException('Please supply a valid identifier.');
}
if(empty($name)) {
throw new \InvalidArgumentException('Please supply a valid name.');
}
if(strlen($price) < 0 || ! is_numeric($price)) {
throw new \InvalidArgumentException('Please supply a valid price.');
}
$this->id = $id;
$this->name = $name;
$this->price = floatval($price);
$this->options = new CartItemOptions($options);
$this->rowId = $this->generateRowId($id, $options);
}
/**
* Returns the formatted price without TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function price()
{
return $this->numberFormat($this->price);
}
/**
* Returns the formatted price with TAX.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function priceTax()
{
return $this->numberFormat($this->priceTax);
}
/**
* Returns the formatted subtotal.
* Subtotal is price for whole CartItem without TAX
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function subtotal()
{
return $this->numberFormat($this->subtotal);
}
/**
* Returns the formatted total.
* Total is price for whole CartItem with TAX
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function total()
{
return $this->numberFormat($this->total);
}
/**
* Returns the formatted tax.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function tax()
{
return $this->numberFormat($this->tax);
}
/**
* Returns the formatted tax.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
public function taxTotal()
{
return $this->numberFormat($this->taxTotal);
}
/**
* Set the quantity for this cart item.
*
* @param int|float $qty
*/
public function setQuantity($qty)
{
if(empty($qty) || ! is_numeric($qty))
throw new \InvalidArgumentException('Please supply a valid quantity.');
$this->qty = $qty;
}
/**
* Update the cart item from a Buyable.
*
* @param Buyable $item
* @return void
*/
public function updateFromBuyable(Buyable $item)
{
$this->id = $item->getBuyableIdentifier($this->options);
$this->name = $item->getBuyableDescription($this->options);
$this->price = $item->getBuyablePrice($this->options);
$this->priceTax = $this->price + $this->tax;
}
/**
* Update the cart item from an array.
*
* @param array $attributes
* @return void
*/
public function updateFromArray(array $attributes)
{
$this->id = Arr::get($attributes, 'id', $this->id);
$this->qty = Arr::get($attributes, 'qty', $this->qty);
$this->name = Arr::get($attributes, 'name', $this->name);
$this->price = Arr::get($attributes, 'price', $this->price);
$this->priceTax = $this->price + $this->tax;
$this->options = new CartItemOptions(Arr::get($attributes, 'options', $this->options));
$this->rowId = $this->generateRowId($this->id, $this->options->all());
}
/**
* Associate the cart item with the given model.
*
* @param mixed $model
* @return CartItem
*/
public function associate($model)
{
$this->associatedModel = is_string($model) ? $model : get_class($model);
return $this;
}
/**
* Set the tax rate.
*
* @param int|float $taxRate
* @return CartItem
*/
public function setTaxRate($taxRate)
{
$this->taxRate = $taxRate;
return $this;
}
/**
* Get an attribute from the cart item or get the associated model.
*
* @param string $attribute
* @return mixed
*/
public function __get($attribute)
{
if(property_exists($this, $attribute)) {
return $this->{$attribute};
}
if($attribute === 'priceTax') {
return number_format($this->price + $this->tax, 2, '.', '');
}
if($attribute === 'subtotal') {
return number_format($this->qty * $this->price, 2, '.', '');
}
if($attribute === 'total') {
return number_format($this->qty * $this->priceTax, 2, '.', '');
}
if($attribute === 'tax') {
return number_format($this->price * ($this->taxRate / 100), 2, '.', '');
}
if($attribute === 'taxTotal') {
return number_format($this->tax * $this->qty, 2, '.', '');
}
if($attribute === 'model' && isset($this->associatedModel)) {
return with(new $this->associatedModel)->find($this->id);
}
return null;
}
/**
* Create a new instance from a Buyable.
*
* @param Buyable $item
* @param array $options
* @return CartItem
*/
public static function fromBuyable(Buyable $item, array $options = [])
{
return new self($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $options);
}
/**
* Create a new instance from the given array.
*
* @param array $attributes
* @return CartItem
*/
public static function fromArray(array $attributes)
{
$options = Arr::get($attributes, 'options', []);
return new self($attributes['id'], $attributes['name'], $attributes['price'], $options);
}
/**
* Create a new instance from the given attributes.
*
* @param int|string $id
* @param string $name
* @param float $price
* @param array $options
* @return CartItem
*/
public static function fromAttributes($id, $name, $price, array $options = [])
{
return new self($id, $name, $price, $options);
}
/**
* Generate a unique id for the cart item.
*
* @param string $id
* @param array $options
* @return string
*/
protected function generateRowId($id, array $options)
{
ksort($options);
return md5($id . serialize($options));
}
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return [
'rowId' => $this->rowId,
'id' => $this->id,
'name' => $this->name,
'qty' => $this->qty,
'price' => $this->price,
'options' => $this->options->toArray(),
'tax' => $this->tax,
'subtotal' => $this->subtotal
];
}
/**
* Convert the object to its JSON representation.
*
* @param int $options
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->toArray(), $options);
}
/**
* Get the formatted number.
*
* @param float $value
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeparator
* @return string
*/
private function numberFormat($value)
{
$decimals = is_null(config('cart.format.decimals')) ? 2 : config('cart.format.decimals');
$decimalPoint = is_null(config('cart.format.decimal_point')) ? '.' : config('cart.format.decimal_point');
$thousandSeparator = '';
return number_format($value, $decimals, $decimalPoint, $thousandSeparator);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Packages\Shoppingcart;
use Illuminate\Support\Collection;
class CartItemOptions extends Collection
{
/**
* Get the option by the given key.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->get($key);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Packages\Shoppingcart\Contracts;
interface Buyable
{
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null);
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null);
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null);
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Packages\Shoppingcart\Contracts;
interface InstanceIdentifier
{
/**
* Get the unique identifier to load the Cart from.
*
* @return int|string
*/
public function getInstanceIdentifier($options = null);
/**
* Get the unique identifier to load the Cart from.
*
* @return int|string
*/
// public function getInstanceGlobalDiscount($options = null);
}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Packages\Shoppingcart\Exceptions;
use RuntimeException;
class CartAlreadyStoredException extends RuntimeException {}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Packages\Shoppingcart\Exceptions;
use RuntimeException;
class InvalidRowIDException extends RuntimeException {}

View file

@ -0,0 +1,7 @@
<?php
namespace App\Packages\Shoppingcart\Exceptions;
use RuntimeException;
class UnknownModelException extends RuntimeException {}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Packages\Shoppingcart\Facades;
use Illuminate\Support\Facades\Facade;
class Cart extends Facade {
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'cart';
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Packages\Shoppingcart;
use Illuminate\Auth\Events\Logout;
use Illuminate\Session\SessionManager;
use Illuminate\Support\ServiceProvider;
class ShoppingcartServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind('cart', 'Gloudemans\Shoppingcart\Cart');
$config = __DIR__ . '/../config/cart.php';
$this->mergeConfigFrom($config, 'cart');
$this->publishes([__DIR__ . '/../config/cart.php' => config_path('cart.php')], 'config');
$this->app['events']->listen(Logout::class, function () {
if ($this->app['config']->get('cart.destroy_on_logout')) {
$this->app->make(SessionManager::class)->forget('cart');
}
});
if ( ! class_exists('CreateShoppingcartTable')) {
// Publish the migration
$timestamp = date('Y_m_d_His', time());
$this->publishes([
__DIR__ . '/../database/migrations/0000_00_00_000000_create_shopping_cart_table.php' => database_path('migrations/'.$timestamp.'_create_shoppingcart_table.php'),
], 'migrations');
}
}
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Rob Gloudemans <info@robgloudemans.nl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,477 @@
## LaravelShoppingcart
A simple shopping cart implementation for Laravel 7 & 8 & 9 & 10.
## Installation
Install the package through [Composer](http://getcomposer.org/).
Run the Composer require command from the Terminal:
composer require mindscms/laravelshoppingcart
### Laravel <= 7.0
Should you still be on version 7.0 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.
Add a new line to the `providers` array:
Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class
And optionally add a new line to the `aliases` array:
'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class,
Now you're ready to start using the shopping cart in your application.
**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**
## Overview
Look at one of the following topics to learn more about LaravelShoppingcart
* [Usage](#usage)
* [Collections](#collections)
* [Instances](#instances)
* [Models](#models)
* [Database](#database)
* [Exceptions](#exceptions)
* [Events](#events)
* [Example](#example)
## Usage
The shoppingcart gives you the following methods to use:
### Cart::add()
Adding an item to the cart is really simple, you just use the `add()` method, which accepts a variety of parameters.
In its most basic form you can specify the id, name, quantity, price of the product you'd like to add to the cart.
```php
Cart::add('293ad', 'Product 1', 1, 9.99);
```
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.
```php
Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large']);
```
**The `add()` method will return an CartItem instance of the item you just added to the cart.**
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.
```php
Cart::add(['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 9.99, 'options' => ['size' => 'large']]);
```
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.
This way you can just pass the `add()` method a model and the quantity and it will automatically add it to the cart.
**As an added bonus it will automatically associate the model with the CartItem**
```php
Cart::add($product, 1, ['size' => 'large']);
```
As an optional third parameter you can add options.
```php
Cart::add($product, 1, ['size' => 'large']);
```
Finally, you can also add multipe items to the cart at once.
You can just pass the `add()` method an array of arrays, or an array of Buyables and they will be added to the cart.
**When adding multiple items to the cart, the `add()` method will return an array of CartItems.**
```php
Cart::add([
['id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 10.00],
['id' => '4832k', 'name' => 'Product 2', 'qty' => 1, 'price' => 10.00, 'options' => ['size' => 'large']]
]);
Cart::add([$product1, $product2]);
```
### Cart::update()
To update an item in the cart, you'll first need the rowId of the item.
Next you can use the `update()` method to update it.
If you simply want to update the quantity, you'll pass the update method the rowId and the new quantity:
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::update($rowId, 2); // Will update the quantity
```
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.
```php
Cart::update($rowId, ['name' => 'Product 1']); // Will update the name
Cart::update($rowId, $product); // Will update the id, name and price
```
### Cart::remove()
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.
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::remove($rowId);
```
### Cart::get()
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.
```php
$rowId = 'da39a3ee5e6b4b0d3255bfef95601890afd80709';
Cart::get($rowId);
```
### Cart::content()
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.
```php
Cart::content();
```
This method will return the content of the current cart instance, if you want the content of another instance, simply chain the calls.
```php
Cart::instance('wishlist')->content();
```
### Cart::destroy()
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.
```php
Cart::destroy();
```
### Cart::total()
The `total()` method can be used to get the calculated total of all items in the cart, given there price and quantity. Includes any additional costs too.
```php
Cart::total();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::total($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::tax()
The `tax()` method can be used to get the calculated amount of tax for all items in the cart, given there price and quantity.
```php
Cart::tax();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::tax($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::subtotal()
The `subtotal()` method can be used to get the total of all items in the cart, minus the total amount of tax.
```php
Cart::subtotal();
```
The method will automatically format the result, which you can tweak using the three optional parameters
```php
Cart::subtotal($decimals, $decimalSeperator, $thousandSeperator);
```
You can set the default number format in the config file.
**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`**
### Cart::count()
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.
```php
Cart::count();
```
### Cart::search()
To find an item in the cart, you can use the `search()` method.
**This method was changed on version 2**
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.
If you for instance want to find all items with an id of 1:
```php
$cart->search(function ($cartItem, $rowId) {
return $cartItem->id === 1;
});
```
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.
**The method will return a Collection containing all CartItems that where found**
This way of searching gives you total control over the search process and gives you the ability to create very precise and specific searches.
### Cart::addCost()
If you want to add additional costs to the cart you can use the `addCost()` method. The method accepts a cost name and the price of the cost. This can be used for eg shipping or transaction costs.
```php
Cart::addCost($name, $price)
```
**Add this method before summarizing the whole cart. The costs are not saved in the session (yet).**
### Cart::getCost()
Get an addition cost you added by `addCost()`. Accepts the cost name. Returns the formatted price of the cost.
```php
Cart::getCost($name, $decimals, $decimalPoint, $thousandSeperator)
```
## Collections
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.
As an example, you can quicky get the number of unique products in a cart:
```php
Cart::content()->count();
```
Or you can group the content by the id of the products:
```php
Cart::content()->groupBy('id');
```
## Instances
The packages supports multiple instances of the cart. The way this works is like this:
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.
If you want to switch instances, you just call `Cart::instance('otherInstance')` again, and you're working with the `otherInstance` again.
So a little example:
```php
Cart::instance('shopping')->add('192ao12', 'Product 1', 1, 9.99);
// Get the content of the 'shopping' cart
Cart::content();
Cart::instance('wishlist')->add('sdjk922', 'Product 2', 1, 19.95, ['size' => 'medium']);
// Get the content of the 'wishlist' cart
Cart::content();
// If you want to get the content of the 'shopping' cart again
Cart::instance('shopping')->content();
// And the count of the 'wishlist' cart again
Cart::instance('wishlist')->count();
```
**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.**
**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()`.**
## Models
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.
That way you can access your model right from the `CartItem`!
The model can be accessed via the `model` property on the CartItem.
**If your model implements the `Buyable` interface and you used your model to add the item to the cart, it will associate automatically.**
Here is an example:
```php
// First we'll add the item to the cart.
$cartItem = Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large']);
// Next we associate a model with the item.
Cart::associate($cartItem->rowId, 'Product');
// Or even easier, call the associate method on the CartItem!
$cartItem->associate('Product');
// You can even make it a one-liner
Cart::add('293ad', 'Product 1', 1, 9.99, ['size' => 'large'])->associate('Product');
// Now, when iterating over the content of the cart, you can access the model.
foreach(Cart::content() as $row) {
echo 'You have ' . $row->qty . ' items of ' . $row->model->name . ' with description: "' . $row->model->description . '" in your cart.';
}
```
## Database
- [Config](#configuration)
- [Storing the cart](#save-cart-to-database)
- [Restoring the cart](#retrieve-cart-from-database)
### Configuration
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.
By default the package will use the default database connection and use a table named `shopping_cart`.
If you want to change these options, you'll have to publish the `config` file.
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config"
This will give you a `cart.php` config file in which you can make the changes.
To make your life easy, the package also includes a ready to use `migration` which you can publish by running:
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="migrations"
This will place a `shopping_cart` table's migration file into `database/migrations` directory. Now all you have to do is run `php artisan migrate` to migrate your database.
### Storing the cart
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.
Cart::store('username');
// To store a cart instance named 'wishlist'
Cart::instance('wishlist')->store('username');
### Restoring the cart
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.
Cart::restore('username');
// To restore a cart instance named 'wishlist'
Cart::instance('wishlist')->restore('username');
## Exceptions
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:
| Exception | Reason |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| *CartAlreadyStoredException* | When trying to store a cart that was already stored using the specified identifier |
| *InvalidRowIDException* | When the rowId that got passed doesn't exists in the current cart instance |
| *UnknownModelException* | When you try to associate an none existing model to a CartItem. |
## Events
The cart also has events build in. There are five events available for you to listen for.
| Event | Fired | Parameter |
| ------------- | ---------------------------------------- | -------------------------------- |
| cart.added | When an item was added to the cart. | The `CartItem` that was added. |
| cart.updated | When an item in the cart was updated. | The `CartItem` that was updated. |
| cart.removed | When an item is removed from the cart. | The `CartItem` that was removed. |
| cart.stored | When the content of a cart was stored. | - |
| cart.restored | When the content of a cart was restored. | - |
## Example
Below is a little example of how to list the cart content in a table:
```php
// Add some items in your Controller.
Cart::add('192ao12', 'Product 1', 1, 9.99);
Cart::add('1239ad0', 'Product 2', 2, 5.95, ['size' => 'large']);
// Set an additional cost (on the same page where you display your cart content)
Cart::addCost(Cart::COST_TRANSACTION, 0.10);
Cart::addCost(Cart::COST_SHIPPING, 5.00);
Cart::addCost('somethingelse', 1.11);
// Display the content in a View.
<table>
<thead>
<tr>
<th>Product</th>
<th>Qty</th>
<th>Price</th>
<th>Subtotal</th>
</tr>
</thead>
<tbody>
<?php foreach(Cart::content() as $row) :?>
<tr>
<td>
<p><strong><?php echo $row->name; ?></strong></p>
<p><?php echo ($row->options->has('size') ? $row->options->size : ''); ?></p>
</td>
<td><input type="text" value="<?php echo $row->qty; ?>"></td>
<td>$<?php echo $row->price; ?></td>
<td>$<?php echo $row->total; ?></td>
</tr>
<?php endforeach;?>
</tbody>
<tfoot>
<tr>
<td colspan="2">&nbsp;</td>
<td>Subtotal</td>
<td><?php echo Cart::subtotal(); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Tax</td>
<td><?php echo Cart::tax(); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Transaction cost</td>
<td><?php echo Cart::getCost(\Gloudemans\Shoppingcart\Cart::COST_TRANSACTION); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Transaction cost</td>
<td><?php echo Cart::getCost(\Gloudemans\Shoppingcart\Cart::COST_SHIPPING); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Transaction cost</td>
<td><?php echo Cart::getCost('somethingelse'); ?></td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>Total</td>
<td><?php echo Cart::total(); ?></td>
</tr>
</tfoot>
</table>
```

View file

@ -0,0 +1,51 @@
{
"name": "mindscms/laravelshoppingcart",
"description": "Laravel Shopping cart",
"keywords": ["laravel", "shoppingcart"],
"license": "MIT",
"authors": [
{
"name": "Rob Gloudemans",
"email": "info@robgloudemans.nl"
},
{
"name": "Michael V.",
"email": "michael@michaelbelgium.me"
},
{
"name": "Sami Mansour",
"email": "mindscms@gmail.com"
}
],
"require": {
"illuminate/support": "^7.0|^8.0|^9.0|^10.0",
"illuminate/session": "^7.0|^8.0|^9.0|^10.0",
"illuminate/events": "^7.0|^8.0|^9.0|^10.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5|^10.1",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^7"
},
"autoload": {
"psr-4": {
"Gloudemans\\Shoppingcart\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Gloudemans\\Tests\\Shoppingcart\\": "tests/"
}
},
"minimum-stability": "stable",
"extra": {
"laravel": {
"providers": [
"Gloudemans\\Shoppingcart\\ShoppingcartServiceProvider"
],
"aliases": {
"Cart": "Gloudemans\\Shoppingcart\\Facades\\Cart"
}
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -0,0 +1,35 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Gloudemans\Shoppingcart\Cart;
use PHPUnit\Framework\Assert as PHPUnit;
trait CartAssertions
{
/**
* Assert that the cart contains the given number of items.
*
* @param int|float $items
* @param \Gloudemans\Shoppingcart\Cart $cart
*/
public function assertItemsInCart($items, Cart $cart)
{
$actual = $cart->count();
PHPUnit::assertEquals($items, $cart->count(), "Expected the cart to contain {$items} items, but got {$actual}.");
}
/**
* Assert that the cart contains the given number of rows.
*
* @param int $rows
* @param \Gloudemans\Shoppingcart\Cart $cart
*/
public function assertRowsInCart($rows, Cart $cart)
{
$actual = $cart->content()->count();
PHPUnit::assertCount($rows, $cart->content(), "Expected the cart to contain {$rows} rows, but got {$actual}.");
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Orchestra\Testbench\TestCase;
use Gloudemans\Shoppingcart\CartItem;
use Gloudemans\Shoppingcart\ShoppingcartServiceProvider;
class CartItemTest extends TestCase
{
/**
* Set the package service provider.
*
* @param \Illuminate\Foundation\Application $app
* @return array
*/
protected function getPackageProviders($app)
{
return [ShoppingcartServiceProvider::class];
}
/** @test */
public function it_can_be_cast_to_an_array()
{
$cartItem = new CartItem(1, 'Some item', 10.00, ['size' => 'XL', 'color' => 'red']);
$cartItem->setQuantity(2);
$this->assertEquals([
'id' => 1,
'name' => 'Some item',
'price' => 10.00,
'rowId' => '07d5da5550494c62daf9993cf954303f',
'qty' => 2,
'options' => [
'size' => 'XL',
'color' => 'red'
],
'tax' => 0,
'subtotal' => 20.00,
], $cartItem->toArray());
}
/** @test */
public function it_can_be_cast_to_json()
{
$cartItem = new CartItem(1, 'Some item', 10.00, ['size' => 'XL', 'color' => 'red']);
$cartItem->setQuantity(2);
$this->assertJson($cartItem->toJson());
$json = '{"rowId":"07d5da5550494c62daf9993cf954303f","id":1,"name":"Some item","qty":2,"price":10,"options":{"size":"XL","color":"red"},"tax":"0.00","subtotal":"20.00"}';
$this->assertEquals($json, $cartItem->toJson());
}
}

View file

@ -0,0 +1,945 @@
<?php
namespace Gloudemans\Tests\Shoppingcart;
use Mockery;
use PHPUnit\Framework\Assert;
use Gloudemans\Shoppingcart\Cart;
use Orchestra\Testbench\TestCase;
use Illuminate\Auth\Events\Logout;
use Illuminate\Support\Collection;
use Gloudemans\Shoppingcart\CartItem;
use Illuminate\Support\Facades\Event;
use Illuminate\Session\SessionManager;
use Illuminate\Contracts\Auth\Authenticatable;
use Gloudemans\Shoppingcart\ShoppingcartServiceProvider;
use Gloudemans\Tests\Shoppingcart\Fixtures\ProductModel;
use Gloudemans\Tests\Shoppingcart\Fixtures\BuyableProduct;
class CartTest extends TestCase
{
use CartAssertions;
/**
* Set the package service provider.
*
* @param \Illuminate\Foundation\Application $app
* @return array
*/
protected function getPackageProviders($app)
{
return [ShoppingcartServiceProvider::class];
}
/**
* Define environment setup.
*
* @param \Illuminate\Foundation\Application $app
* @return void
*/
protected function getEnvironmentSetUp($app)
{
$app['config']->set('cart.database.connection', 'testing');
$app['config']->set('session.driver', 'array');
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
/**
* Setup the test environment.
*
* @return void
*/
protected function setUp()
{
parent::setUp();
$this->app->afterResolving('migrator', function ($migrator) {
$migrator->path(realpath(__DIR__.'/../database/migrations'));
});
}
/** @test */
public function it_has_a_default_instance()
{
$cart = $this->getCart();
$this->assertEquals(Cart::DEFAULT_INSTANCE, $cart->currentInstance());
}
/** @test */
public function it_can_have_multiple_instances()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item'));
$cart->instance('wishlist')->add(new BuyableProduct(2, 'Second item'));
$this->assertItemsInCart(1, $cart->instance(Cart::DEFAULT_INSTANCE));
$this->assertItemsInCart(1, $cart->instance('wishlist'));
}
/** @test */
public function it_can_add_an_item()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_will_return_the_cartitem_of_the_added_item()
{
Event::fake();
$cart = $this->getCart();
$cartItem = $cart->add(new BuyableProduct);
$this->assertInstanceOf(CartItem::class, $cartItem);
$this->assertEquals('027c91341fd5cf4d2579b49c4b6a90da', $cartItem->rowId);
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_multiple_buyable_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cart->add([new BuyableProduct(1), new BuyableProduct(2)]);
$this->assertEquals(2, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_will_return_an_array_of_cartitems_when_you_add_multiple_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cartItems = $cart->add([new BuyableProduct(1), new BuyableProduct(2)]);
$this->assertTrue(is_array($cartItems));
$this->assertCount(2, $cartItems);
$this->assertContainsOnlyInstancesOf(CartItem::class, $cartItems);
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_from_attributes()
{
Event::fake();
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_from_an_array()
{
Event::fake();
$cart = $this->getCart();
$cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => 10.00]);
$this->assertEquals(1, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_multiple_array_items_at_once()
{
Event::fake();
$cart = $this->getCart();
$cart->add([
['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => 10.00],
['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => 10.00]
]);
$this->assertEquals(2, $cart->count());
Event::assertDispatched('cart.added');
}
/** @test */
public function it_can_add_an_item_with_options()
{
Event::fake();
$cart = $this->getCart();
$options = ['size' => 'XL', 'color' => 'red'];
$cart->add(new BuyableProduct, 1, $options);
$cartItem = $cart->get('07d5da5550494c62daf9993cf954303f');
$this->assertInstanceOf(CartItem::class, $cartItem);
$this->assertEquals('XL', $cartItem->options->size);
$this->assertEquals('red', $cartItem->options->color);
Event::assertDispatched('cart.added');
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid identifier.
*/
public function it_will_validate_the_identifier()
{
$cart = $this->getCart();
$cart->add(null, 'Some title', 1, 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid name.
*/
public function it_will_validate_the_name()
{
$cart = $this->getCart();
$cart->add(1, null, 1, 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid quantity.
*/
public function it_will_validate_the_quantity()
{
$cart = $this->getCart();
$cart->add(1, 'Some title', 'invalid', 10.00);
}
/**
* @test
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Please supply a valid price.
*/
public function it_will_validate_the_price()
{
$cart = $this->getCart();
$cart->add(1, 'Some title', 1, 'invalid');
}
/** @test */
public function it_will_update_the_cart_if_the_item_already_exists_in_the_cart()
{
$cart = $this->getCart();
$item = new BuyableProduct;
$cart->add($item);
$cart->add($item);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_will_keep_updating_the_quantity_when_an_item_is_added_multiple_times()
{
$cart = $this->getCart();
$item = new BuyableProduct;
$cart->add($item);
$cart->add($item);
$cart->add($item);
$this->assertItemsInCart(3, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_can_update_the_quantity_of_an_existing_item_in_the_cart()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', 2);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
Event::assertDispatched('cart.updated');
}
/** @test */
public function it_can_update_an_existing_item_in_the_cart_from_a_buyable()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', new BuyableProduct(1, 'Different description'));
$this->assertItemsInCart(1, $cart);
$this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name);
Event::assertDispatched('cart.updated');
}
/** @test */
public function it_can_update_an_existing_item_in_the_cart_from_an_array()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', ['name' => 'Different description']);
$this->assertItemsInCart(1, $cart);
$this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name);
Event::assertDispatched('cart.updated');
}
/**
* @test
* @expectedException \Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException
*/
public function it_will_throw_an_exception_if_a_rowid_was_not_found()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('none-existing-rowid', new BuyableProduct(1, 'Different description'));
}
/** @test */
public function it_will_regenerate_the_rowid_if_the_options_changed()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct, 1, ['color' => 'red']);
$cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => ['color' => 'blue']]);
$this->assertItemsInCart(1, $cart);
$this->assertEquals('7e70a1e9aaadd18c72921a07aae5d011', $cart->content()->first()->rowId);
$this->assertEquals('blue', $cart->get('7e70a1e9aaadd18c72921a07aae5d011')->options->color);
}
/** @test */
public function it_will_add_the_item_to_an_existing_row_if_the_options_changed_to_an_existing_rowid()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct, 1, ['color' => 'red']);
$cart->add(new BuyableProduct, 1, ['color' => 'blue']);
$cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => ['color' => 'red']]);
$this->assertItemsInCart(2, $cart);
$this->assertRowsInCart(1, $cart);
}
/** @test */
public function it_can_remove_an_item_from_the_cart()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->remove('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_will_remove_the_item_if_its_quantity_was_set_to_zero()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', 0);
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_will_remove_the_item_if_its_quantity_was_set_negative()
{
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->update('027c91341fd5cf4d2579b49c4b6a90da', -1);
$this->assertItemsInCart(0, $cart);
$this->assertRowsInCart(0, $cart);
Event::assertDispatched('cart.removed');
}
/** @test */
public function it_can_get_an_item_from_the_cart_by_its_rowid()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertInstanceOf(CartItem::class, $cartItem);
}
/** @test */
public function it_can_get_the_content_of_the_cart()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1));
$cart->add(new BuyableProduct(2));
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertCount(2, $content);
}
/** @test */
public function it_will_return_an_empty_collection_if_the_cart_is_empty()
{
$cart = $this->getCart();
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertCount(0, $content);
}
/** @test */
public function it_will_include_the_tax_and_subtotal_when_converted_to_an_array()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1));
$cart->add(new BuyableProduct(2));
$content = $cart->content();
$this->assertInstanceOf(Collection::class, $content);
$this->assertEquals([
'027c91341fd5cf4d2579b49c4b6a90da' => [
'rowId' => '027c91341fd5cf4d2579b49c4b6a90da',
'id' => 1,
'name' => 'Item name',
'qty' => 1,
'price' => 10.00,
'tax' => 2.10,
'subtotal' => 10.0,
'options' => [],
],
'370d08585360f5c568b18d1f2e4ca1df' => [
'rowId' => '370d08585360f5c568b18d1f2e4ca1df',
'id' => 2,
'name' => 'Item name',
'qty' => 1,
'price' => 10.00,
'tax' => 2.10,
'subtotal' => 10.0,
'options' => [],
]
], $content->toArray());
}
/** @test */
public function it_can_destroy_a_cart()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$this->assertItemsInCart(1, $cart);
$cart->destroy();
$this->assertItemsInCart(0, $cart);
}
/** @test */
public function it_can_get_the_total_price_of_the_cart_content()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 10.00));
$cart->add(new BuyableProduct(2, 'Second item', 25.00), 2);
$this->assertItemsInCart(3, $cart);
$this->assertEquals(60.00, $cart->subtotal());
}
/** @test */
public function it_can_return_a_formatted_total()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 1000.00));
$cart->add(new BuyableProduct(2, 'Second item', 2500.00), 2);
$this->assertItemsInCart(3, $cart);
$this->assertEquals('6.000,00', $cart->subtotal(2, ',', '.'));
}
/** @test */
public function it_can_search_the_cart_for_a_specific_item()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'));
$cart->add(new BuyableProduct(2, 'Another item'));
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->name == 'Some item';
});
$this->assertInstanceOf(Collection::class, $cartItem);
$this->assertCount(1, $cartItem);
$this->assertInstanceOf(CartItem::class, $cartItem->first());
$this->assertEquals(1, $cartItem->first()->id);
}
/** @test */
public function it_can_search_the_cart_for_multiple_items()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'));
$cart->add(new BuyableProduct(2, 'Some item'));
$cart->add(new BuyableProduct(3, 'Another item'));
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->name == 'Some item';
});
$this->assertInstanceOf(Collection::class, $cartItem);
}
/** @test */
public function it_can_search_the_cart_for_a_specific_item_with_options()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some item'), 1, ['color' => 'red']);
$cart->add(new BuyableProduct(2, 'Another item'), 1, ['color' => 'blue']);
$cartItem = $cart->search(function ($cartItem, $rowId) {
return $cartItem->options->color == 'red';
});
$this->assertInstanceOf(Collection::class, $cartItem);
$this->assertCount(1, $cartItem);
$this->assertInstanceOf(CartItem::class, $cartItem->first());
$this->assertEquals(1, $cartItem->first()->id);
}
/** @test */
public function it_will_associate_the_cart_item_with_a_model_when_you_add_a_buyable()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertContains(BuyableProduct::class, Assert::readAttribute($cartItem, 'associatedModel'));
}
/** @test */
public function it_can_associate_the_cart_item_with_a_model()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(ProductModel::class, Assert::readAttribute($cartItem, 'associatedModel'));
}
/**
* @test
* @expectedException \Gloudemans\Shoppingcart\Exceptions\UnknownModelException
* @expectedExceptionMessage The supplied model SomeModel does not exist.
*/
public function it_will_throw_an_exception_when_a_non_existing_model_is_being_associated()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', 'SomeModel');
}
/** @test */
public function it_can_get_the_associated_model_of_a_cart_item()
{
$cart = $this->getCart();
$cart->add(1, 'Test item', 1, 10.00);
$cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertInstanceOf(ProductModel::class, $cartItem->model);
$this->assertEquals('Some value', $cartItem->model->someValue);
}
/** @test */
public function it_can_calculate_the_subtotal_of_a_cart_item()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 9.99), 3);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(29.97, $cartItem->subtotal);
}
/** @test */
public function it_can_return_a_formatted_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 500), 3);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('1.500,00', $cartItem->subtotal(2, ',', '.'));
}
/** @test */
public function it_can_calculate_tax_based_on_the_default_tax_rate_in_the_config()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(2.10, $cartItem->tax);
}
/** @test */
public function it_can_calculate_tax_based_on_the_specified_tax()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals(1.90, $cartItem->tax);
}
/** @test */
public function it_can_return_the_calculated_tax_formatted()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10000.00), 1);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('2.100,00', $cartItem->tax(2, ',', '.'));
}
/** @test */
public function it_can_calculate_the_total_tax_for_all_cart_items()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 20.00), 2);
$this->assertEquals(10.50, $cart->tax);
}
/** @test */
public function it_can_return_formatted_total_tax()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('1.050,00', $cart->tax(2, ',', '.'));
}
/** @test */
public function it_can_return_the_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 10.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 20.00), 2);
$this->assertEquals(50.00, $cart->subtotal);
}
/** @test */
public function it_can_return_formatted_subtotal()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('5000,00', $cart->subtotal(2, ',', ''));
}
/** @test */
public function it_can_return_cart_formated_numbers_by_config_values()
{
$this->setConfigFormat(2, ',', '');
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 1000.00), 1);
$cart->add(new BuyableProduct(2, 'Some title', 2000.00), 2);
$this->assertEquals('5000,00', $cart->subtotal());
$this->assertEquals('1050,00', $cart->tax());
$this->assertEquals('6050,00', $cart->total());
$this->assertEquals('5000,00', $cart->subtotal);
$this->assertEquals('1050,00', $cart->tax);
$this->assertEquals('6050,00', $cart->total);
}
/** @test */
public function it_can_return_cartItem_formated_numbers_by_config_values()
{
$this->setConfigFormat(2, ',', '');
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'Some title', 2000.00), 2);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$this->assertEquals('2000,00', $cartItem->price());
$this->assertEquals('2420,00', $cartItem->priceTax());
$this->assertEquals('4000,00', $cartItem->subtotal());
$this->assertEquals('4840,00', $cartItem->total());
$this->assertEquals('420,00', $cartItem->tax());
$this->assertEquals('840,00', $cartItem->taxTotal());
}
/** @test */
public function it_can_store_the_cart_in_a_database()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$serialized = serialize($cart->content());
$this->assertDatabaseHas('shopping_cart', ['identifier' => $identifier, 'instance' => 'default', 'content' => $serialized]);
Event::assertDispatched('cart.stored');
}
/**
* @test
* @expectedException \Gloudemans\Shoppingcart\Exceptions\CartAlreadyStoredException
* @expectedExceptionMessage A cart with identifier 123 was already stored.
*/
public function it_will_throw_an_exception_when_a_cart_was_already_stored_using_the_specified_identifier()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$cart->store($identifier);
Event::assertDispatched('cart.stored');
}
/** @test */
public function it_can_restore_a_cart_from_the_database()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
Event::fake();
$cart = $this->getCart();
$cart->add(new BuyableProduct);
$cart->store($identifier = 123);
$cart->destroy();
$this->assertItemsInCart(0, $cart);
$cart->restore($identifier);
$this->assertItemsInCart(1, $cart);
$this->assertDatabaseMissing('shopping_cart', ['identifier' => $identifier, 'instance' => 'default']);
Event::assertDispatched('cart.restored');
}
/** @test */
public function it_will_just_keep_the_current_instance_if_no_cart_with_the_given_identifier_was_stored()
{
$this->artisan('migrate', [
'--database' => 'testing',
]);
$cart = $this->getCart();
$cart->restore($identifier = 123);
$this->assertItemsInCart(0, $cart);
}
/** @test */
public function it_can_calculate_all_values()
{
$cart = $this->getCart();
$cart->add(new BuyableProduct(1, 'First item', 10.00), 2);
$cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da');
$cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19);
$this->assertEquals(10.00, $cartItem->price(2));
$this->assertEquals(11.90, $cartItem->priceTax(2));
$this->assertEquals(20.00, $cartItem->subtotal(2));
$this->assertEquals(23.80, $cartItem->total(2));
$this->assertEquals(1.90, $cartItem->tax(2));
$this->assertEquals(3.80, $cartItem->taxTotal(2));
$this->assertEquals(20.00, $cart->subtotal(2));
$this->assertEquals(23.80, $cart->total(2));
$this->assertEquals(3.80, $cart->tax(2));
}
/** @test */
public function it_will_destroy_the_cart_when_the_user_logs_out_and_the_config_setting_was_set_to_true()
{
$this->app['config']->set('cart.destroy_on_logout', true);
$this->app->instance(SessionManager::class, Mockery::mock(SessionManager::class, function ($mock) {
$mock->shouldReceive('forget')->once()->with('cart');
}));
$user = Mockery::mock(Authenticatable::class);
event(new Logout($user));
}
/**
* Get an instance of the cart.
*
* @return \Gloudemans\Shoppingcart\Cart
*/
private function getCart()
{
$session = $this->app->make('session');
$events = $this->app->make('events');
return new Cart($session, $events);
}
/**
* Set the config number format.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
*/
private function setConfigFormat($decimals, $decimalPoint, $thousandSeperator)
{
$this->app['config']->set('cart.format.decimals', $decimals);
$this->app['config']->set('cart.format.decimal_point', $decimalPoint);
$this->app['config']->set('cart.format.thousand_separator', $thousandSeperator);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Gloudemans\Tests\Shoppingcart\Fixtures;
use Gloudemans\Shoppingcart\Contracts\Buyable;
class BuyableProduct implements Buyable
{
/**
* @var int|string
*/
private $id;
/**
* @var string
*/
private $name;
/**
* @var float
*/
private $price;
/**
* BuyableProduct constructor.
*
* @param int|string $id
* @param string $name
* @param float $price
*/
public function __construct($id = 1, $name = 'Item name', $price = 10.00)
{
$this->id = $id;
$this->name = $name;
$this->price = $price;
}
/**
* Get the identifier of the Buyable item.
*
* @return int|string
*/
public function getBuyableIdentifier($options = null)
{
return $this->id;
}
/**
* Get the description or title of the Buyable item.
*
* @return string
*/
public function getBuyableDescription($options = null)
{
return $this->name;
}
/**
* Get the price of the Buyable item.
*
* @return float
*/
public function getBuyablePrice($options = null)
{
return $this->price;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Gloudemans\Tests\Shoppingcart\Fixtures;
class ProductModel
{
public $someValue = 'Some value';
public function find($id)
{
return $this;
}
}

View file

@ -0,0 +1,67 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default tax rate
|--------------------------------------------------------------------------
|
| This default tax rate will be used when you make a class implement the
| Taxable interface and use the HasTax trait.
|
*/
'tax' => 15,
/*
|--------------------------------------------------------------------------
| Shopping cart database settings
|--------------------------------------------------------------------------
|
| Here you can set the connection that the shopping cart should use when
| storing and restoring a cart.
|
*/
'database' => [
'connection' => env('DB_CONNECTION', 'mysql'),
'table' => 'shopping_cart',
],
/*
|--------------------------------------------------------------------------
| Destroy the cart on user logout
|--------------------------------------------------------------------------
|
| When this option is set to 'true' the cart will automatically
| destroy all cart instances when the user logs out.
|
*/
'destroy_on_logout' => false,
/*
|--------------------------------------------------------------------------
| Default number format
|--------------------------------------------------------------------------
|
| This defaults will be used for the formated numbers if you don't
| set them in the method call.
|
*/
'format' => [
'decimals' => 2,
'decimal_point' => '.',
'thousand_separator' => ''
],
];

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateShoppingCartTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('shopping_cart', function (Blueprint $table) {
$table->string('identifier');
$table->string('instance');
$table->longText('content');
$table->nullableTimestamps();
$table->primary(['identifier', 'instance']);
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::drop('shopping_cart');
}
}