mirror of
https://github.com/wearepixel/laravel-cart.git
synced 2026-11-14 13:51:44 +00:00
feat: database support
This commit is contained in:
@@ -22,6 +22,7 @@ ## Table of Contents
|
||||
- [Conditions](#conditions)
|
||||
- [Cart Items](#cart-items)
|
||||
- [Database Support](#database-support)
|
||||
- [Events](#events)
|
||||
- [Credits](#-credits)
|
||||
- [License](#-license)
|
||||
|
||||
@@ -685,29 +686,149 @@ ### Cart Items
|
||||
$item->getPriceSumWithConditions();
|
||||
```
|
||||
|
||||
### Database Support
|
||||
### Storage Options
|
||||
|
||||
Sometimes you might want to store the cart in the database so that the cart can be retrieved even after the user logs out or closes the browser.
|
||||
By default the cart is stored in the session, but there are times you may want to store the cart in the database.
|
||||
|
||||
You may also find yourself wanting to add cart timeouts and support for multiple computers.
|
||||
For instance, you may want to store the cart in the database so that the cart can be retrieved even after the user logs out or closes the browser, or you may want to add cart timeouts and support for multiple computers.
|
||||
|
||||
To get started, you'll need to create a new model for your Cart, the package requires only 3 columns, but you're free to extend this as you wish:
|
||||
#### Session
|
||||
|
||||
The cart is stored in the session by default, using Laravel's in-built SessionManager.
|
||||
|
||||
#### Database Support
|
||||
|
||||
To get started, you'll need to create a new model for your Cart, the package requires only 3 columns, but you're free to extend this as you wish and add more columns.
|
||||
|
||||
You can utilise the [events](#events) provided by the package to store additional information alongside the cart.
|
||||
|
||||
```php
|
||||
$table->string('session_id');
|
||||
$table->text('items');
|
||||
$table->text('conditions');
|
||||
$table->string('session_id'); // this handles the session id of the cart
|
||||
$table->text('items'); // this will store the cart items
|
||||
$table->text('conditions'); // this will store the cart level conditions
|
||||
```
|
||||
|
||||
Then add some json casts to your model:
|
||||
Then add some json casts to your model and fillable columns:
|
||||
|
||||
```php
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'items' => 'array',
|
||||
'conditions' => 'array',
|
||||
];
|
||||
```
|
||||
|
||||
Then update the configuration file to use the database driver:
|
||||
|
||||
```php
|
||||
'driver' => 'database',
|
||||
|
||||
'storage' => [
|
||||
'session',
|
||||
'database' => [
|
||||
'model' => \App\Models\Cart::class, // your model here
|
||||
'id' => 'session_id',
|
||||
'items' => 'items',
|
||||
'conditions' => 'conditions',
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
The package provides a few events that you can listen to in order to manipulate the cart or take actions based on cart events.
|
||||
|
||||
#### LaravelCart.Created
|
||||
|
||||
This fires every time a cart is instantiated (i.e every time \Cart::add() is called)
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Added', function () {
|
||||
// cart was created
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Adding
|
||||
|
||||
This fires every time an item is being added to the cart
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Adding', function ($item) {
|
||||
// item is being added
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Added
|
||||
|
||||
This fires every time an item is successfully added to the cart
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Added', function ($item) {
|
||||
// item was added
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Updating
|
||||
|
||||
This fires every time an item is being updated
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Updating', function ($item) {
|
||||
// item is being updated
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Updated
|
||||
|
||||
This fires every time an item is successfully updated
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Updated', function ($item) {
|
||||
// item was updated
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Removing
|
||||
|
||||
This fires every time an item is being removed
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Removing', function ($item) {
|
||||
// item is being removed
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Removed
|
||||
|
||||
This fires every time an item is successfully removed
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Removed', function ($item) {
|
||||
// item was removed
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Clearing
|
||||
|
||||
This fires every time the cart is being cleared
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Clearing', function () {
|
||||
// cart is being cleared
|
||||
});
|
||||
```
|
||||
|
||||
#### LaravelCart.Cleared
|
||||
|
||||
This fires every time the cart is successfully cleared
|
||||
|
||||
```
|
||||
Event::listen('LaravelCart.Cleared', function () {
|
||||
// cart was cleared
|
||||
});
|
||||
```
|
||||
|
||||
## 🫡 Credits
|
||||
|
||||
This package was orignally created by [darryldecode](https://github.com/darryldecode) but has since seen almost no updates. I have decided to take the old package and transform it into a new package with new features and updates.
|
||||
|
||||
+12
-1
@@ -21,20 +21,31 @@
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"illuminate/auth": "^10.0|^11.0|^12.0",
|
||||
"illuminate/container": "^10.0|^11.0|^12.0",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0",
|
||||
"illuminate/database": "^10.0|^11.0|^12.0",
|
||||
"illuminate/validation": "^10.0|^11.0|^12.0",
|
||||
"illuminate/translation": "^10.0|^11.0|^12.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.4.2",
|
||||
"laravel/pint": "^1.21",
|
||||
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
|
||||
"pestphp/pest": "^3.0",
|
||||
"larapack/dd": "dev-master"
|
||||
"larapack/dd": "dev-master",
|
||||
"illuminate/testing": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Joelwmale\\Cart\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
|
||||
Generated
+4562
-826
File diff suppressed because it is too large
Load Diff
@@ -14,4 +14,7 @@
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="DB_CONNECTION" value="testing"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
+18
-13
@@ -15,11 +15,6 @@ class Cart
|
||||
|
||||
protected $instanceName;
|
||||
|
||||
protected $customStorage = false;
|
||||
protected $sessionKey;
|
||||
protected $sessionKeyCartItems;
|
||||
protected $sessionKeyCartConditions;
|
||||
|
||||
protected $config;
|
||||
|
||||
protected $currentItemId;
|
||||
@@ -168,7 +163,7 @@ public function add(
|
||||
*/
|
||||
public function update(int|string $id, array $data): bool
|
||||
{
|
||||
if ($this->fireEvent('Updating', $data) === false) {
|
||||
if ($this->fireEvent('Updating', 'item', $data) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -201,7 +196,7 @@ public function update(int|string $id, array $data): bool
|
||||
|
||||
$this->save($cart);
|
||||
|
||||
$this->fireEvent('Updated', $item);
|
||||
$this->fireEvent('Updated', 'item', $item);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -260,7 +255,7 @@ public function remove($id)
|
||||
{
|
||||
$cart = $this->getContent();
|
||||
|
||||
if ($this->fireEvent('Removing', $id) === false) {
|
||||
if ($this->fireEvent('Removing', 'item', $id) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -268,7 +263,7 @@ public function remove($id)
|
||||
|
||||
$this->save($cart);
|
||||
|
||||
$this->fireEvent('Removed', $id);
|
||||
$this->fireEvent('Removed', 'item', $id);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -714,7 +709,7 @@ protected function validate($item)
|
||||
*/
|
||||
protected function addItem($id, $item)
|
||||
{
|
||||
if ($this->fireEvent('Adding', $item) === false) {
|
||||
if ($this->fireEvent('Adding', 'item', $item) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -724,7 +719,7 @@ protected function addItem($id, $item)
|
||||
|
||||
$this->save($cart);
|
||||
|
||||
$this->fireEvent('Added', $item);
|
||||
$this->fireEvent('Added', 'item', $item);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -815,9 +810,19 @@ public function setDecimals($decimals)
|
||||
$this->decimals = $decimals;
|
||||
}
|
||||
|
||||
protected function fireEvent($name, $value = [])
|
||||
protected function fireEvent($name, $type = null, $data = null)
|
||||
{
|
||||
return $this->events->dispatch('LaravelCart.' . $name, array_values([$value, $this]), true);
|
||||
$eventData = [
|
||||
'session_key' => $this->session->sessionKey,
|
||||
'session_driver' => $this->session->driver,
|
||||
'session_model' => $this->session->getSessionModel(),
|
||||
];
|
||||
|
||||
if ($type) {
|
||||
$eventData[$type] = $data;
|
||||
}
|
||||
|
||||
return $this->events->dispatch('LaravelCart.' . $name, array_values([$eventData, $this]), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,7 +46,7 @@ public function register()
|
||||
$storage,
|
||||
$events,
|
||||
'cart',
|
||||
'4yTlTDKu3oJOfzD',
|
||||
session()->getId(),
|
||||
$config
|
||||
);
|
||||
});
|
||||
|
||||
+16
-5
@@ -6,6 +6,9 @@ class CartSession
|
||||
{
|
||||
private $customStorage;
|
||||
|
||||
public $sessionKey;
|
||||
public $driver;
|
||||
|
||||
private $session;
|
||||
|
||||
private $sessionId = null;
|
||||
@@ -14,12 +17,15 @@ class CartSession
|
||||
|
||||
public function __construct($session, $sessionKey, $config)
|
||||
{
|
||||
if ($config['driver'] === 'session') {
|
||||
$this->sessionKey = $sessionKey;
|
||||
$this->driver = $config['driver'];
|
||||
|
||||
if ($this->driver === 'session') {
|
||||
$this->customStorage = false;
|
||||
$this->session = $session;
|
||||
$this->itemsKey = $sessionKey . '_cart_items';
|
||||
$this->conditionsKey = $sessionKey . '_cart_conditions';
|
||||
} elseif ($config['driver'] === 'database') {
|
||||
$this->itemsKey = $this->sessionKey . '_cart_items';
|
||||
$this->conditionsKey = $this->sessionKey . '_cart_conditions';
|
||||
} elseif ($this->driver === 'database') {
|
||||
$this->customStorage = true;
|
||||
|
||||
$this->sessionId = $config['storage']['database']['id'];
|
||||
@@ -27,7 +33,7 @@ public function __construct($session, $sessionKey, $config)
|
||||
$this->conditionsKey = $config['storage']['database']['conditions'];
|
||||
|
||||
// find or create the session in the database
|
||||
$this->session = $session->firstOrNew([$this->sessionId => $sessionKey]);
|
||||
$this->session = $session->firstOrNew([$this->sessionId => $this->sessionKey]);
|
||||
|
||||
// initialize the items and conditions
|
||||
$this->session[$this->itemsKey] = $this->session[$this->itemsKey] ?? [];
|
||||
@@ -99,4 +105,9 @@ public function clear()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getSessionModel()
|
||||
{
|
||||
return $this->session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'format_numbers' => true,
|
||||
'decimals' => 2,
|
||||
'round_mode' => 'up',
|
||||
'driver' => 'database',
|
||||
'storage' => [
|
||||
'session',
|
||||
'database' => [
|
||||
'model' => \Joelwmale\Cart\Tests\Helpers\MockCartModel::class,
|
||||
'id' => 'session_id',
|
||||
'items' => 'items',
|
||||
'conditions' => 'conditions',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Joelwmale\Cart\Tests\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MockCartModel extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'session_id',
|
||||
'items',
|
||||
'conditions',
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'items' => 'array',
|
||||
'conditions' => 'array',
|
||||
];
|
||||
|
||||
protected $table = 'carts';
|
||||
}
|
||||
+2
-26
@@ -1,29 +1,5 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
use Tests\TestCase;
|
||||
|
||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
uses(TestCase::class)->in(__DIR__);
|
||||
|
||||
+44
-2
@@ -1,10 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Events\Dispatcher;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
protected DB $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a Laravel container
|
||||
$container = Container::getInstance();
|
||||
$dispatcher = new Dispatcher;
|
||||
|
||||
// Setup Capsule (Eloquent ORM)
|
||||
$this->db = new DB($container);
|
||||
$this->db->addConnection([
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]);
|
||||
$this->db->setAsGlobal();
|
||||
$this->db->bootEloquent();
|
||||
$this->db->getConnection()->setEventDispatcher($dispatcher);
|
||||
|
||||
// Run migrations (optional, for testing models)
|
||||
$this->migrateDatabase();
|
||||
}
|
||||
|
||||
protected function migrateDatabase()
|
||||
{
|
||||
$schema = $this->db->schema();
|
||||
|
||||
$schema->create('carts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('session_id');
|
||||
$table->text('items');
|
||||
$table->text('conditions');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Joelwmale\Cart\Cart;
|
||||
use Joelwmale\Cart\Tests\Helpers\MockCartModel;
|
||||
|
||||
beforeEach(function () {
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$storage = new MockCartModel;
|
||||
|
||||
$this->cart = new Cart(
|
||||
$storage,
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigDatabaseMock.php')
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
describe('database cart', function () {
|
||||
test('can add an item', function () {
|
||||
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||
|
||||
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||
expect($this->cart->getContent()->first()['price'])->toEqual(100.99, 'Item added has price of 100.99 so first content price should be 100.99');
|
||||
|
||||
expect(MockCartModel::all()->count())->toEqual(1, 'The carts database table should have 1 row');
|
||||
});
|
||||
|
||||
test('can add and remove an item', function () {
|
||||
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||
|
||||
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||
expect($this->cart->getContent()->first()['price'])->toEqual(100.99, 'Item added has price of 100.99 so first content price should be 100.99');
|
||||
|
||||
expect(MockCartModel::all()->count())->toEqual(1, 'The carts database table should have 1 row');
|
||||
|
||||
$this->cart->remove(1);
|
||||
|
||||
$cartModel = MockCartModel::first();
|
||||
|
||||
expect($this->cart->isEmpty())->toBeTrue('Cart should be empty');
|
||||
expect($this->cart->getContent()->count())->toEqual(0, 'Cart content should be 0');
|
||||
expect($cartModel->items)->toEqual([], 'The carts database table should have 0 row');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user