feat: database support

This commit is contained in:
Joel Male
2025-02-25 16:03:17 +10:00
parent ec99aa0563
commit 0f07b9ae8a
12 changed files with 4882 additions and 882 deletions
+129 -8
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -14,4 +14,7 @@
<directory>tests</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
+18 -13
View File
@@ -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);
}
/**
+1 -1
View File
@@ -46,7 +46,7 @@ public function register()
$storage,
$events,
'cart',
'4yTlTDKu3oJOfzD',
session()->getId(),
$config
);
});
+16 -5
View File
@@ -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;
}
}
+17
View File
@@ -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',
],
],
];
+23
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
}
}
+55
View File
@@ -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');
});
});