Compare commits

..
47 Commits
Author SHA1 Message Date
Joel Male 7427ce953a chore: bump version to 2.1.4 2026-06-13 14:19:39 +10:00
Joel Male 6ba50a1c48 fix: prevent save() on array after clear() with database driver 2026-06-13 14:18:55 +10:00
github-actions[bot] fce7125c5f Merge pull request #14 from wearepixel/dependabot/github_actions/actions-5dacb66663
chore(deps): bump shivammathur/setup-php from 2.37.1 to 2.37.2 in the actions group
2026-06-08 21:45:38 +00:00
dependabot[bot] 834e211443 chore(deps): bump shivammathur/setup-php in the actions group
Bumps the actions group with 1 update: [shivammathur/setup-php](https://github.com/shivammathur/setup-php).


Updates `shivammathur/setup-php` from 2.37.1 to 2.37.2
- [Release notes](https://github.com/shivammathur/setup-php/releases)
- [Commits](https://github.com/shivammathur/setup-php/compare/7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc...f3e473d116dcccaddc5834248c87452386958240)

---
updated-dependencies:
- dependency-name: shivammathur/setup-php
  dependency-version: 2.37.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 21:45:26 +00:00
github-actions[bot] 9b198edb7d Merge pull request #13 from wearepixel/dependabot/github_actions/actions-6a98abd9ac
chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 in the actions group
2026-06-03 10:48:17 +00:00
dependabot[bot] 49959faace chore(deps): bump actions/checkout in the actions group
Bumps the actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 10:48:04 +00:00
Joel Male e61ffae5fe chore: bump version to 2.1.3 2026-05-29 14:27:02 +10:00
Joel Male 10ab30f4e8 fix: correct add() @method signature to accept array input style 2026-05-29 14:14:34 +10:00
Joel Male 34626eef5e chore: bump version to 2.1.2 2026-05-29 14:12:49 +10:00
Joel Male 6020b655e2 Merge pull request #12 from wearepixel/fix/phpstan-facade-types
fix: add PHPStan/Larastan support to CartFacade
2026-05-29 14:11:47 +10:00
Joel Male 2c56062f97 fix: add @mixin and @method PHPDoc to CartFacade for PHPStan/Larastan support 2026-05-29 14:09:15 +10:00
Joel Male b2a4864675 chore: ignore docs/superpowers/ 2026-05-27 20:22:32 +10:00
Joel Male aaa3360d1f chore: add test bootstrap, update phpunit config, and add Laravel 13 support to README
- Add tests/bootstrap.php to suppress PHP 8.4 vendor deprecation notices (Mockery implicit-nullable)
- Update phpunit.xml.dist to reference bootstrap, use PHPUnit 11.5 schema, and ignore indirect deprecations
- Note Laravel 13 support in README
2026-05-27 19:50:09 +10:00
Joel Male 05729b7865 chore: bump version to 2.1.1 2026-05-27 16:42:52 +10:00
Joel Male 7e6ee749bf Merge pull request #10 from wearepixel/dependabot/composer/pestphp/pest-4.7.0
chore(deps-dev): bump pestphp/pest from 3.x-dev to 4.7.0
2026-05-27 16:42:24 +10:00
Joel Male 17532f1b05 chore(deps-dev): bump pestphp/pest from 3.x-dev to 4.7.0
Resolves merge conflict with laravel/pint update - regenerated lock file
to include both pint 1.29.1 and pest 4.7.0 with all dependencies.
2026-05-27 16:42:16 +10:00
Joel Male d058ad5521 Merge pull request #9 from wearepixel/dependabot/composer/php-minor-patch-d003f11a51
chore(deps-dev): bump laravel/pint from 1.21.0 to 1.29.1 in the php-minor-patch group
2026-05-27 16:41:05 +10:00
Joel Male 3f5e9374c3 Merge pull request #8 from wearepixel/dependabot/github_actions/actions-c77b365b7e
chore(deps): bump the actions group across 1 directory with 3 updates
2026-05-27 16:41:02 +10:00
Joel Male fcdd356718 fix: update CI runner from deprecated ubuntu-20.04 to ubuntu-latest 2026-05-27 16:40:41 +10:00
dependabot[bot] 3500fa43a9 chore(deps): bump the actions group across 1 directory with 3 updates
Bumps the actions group with 3 updates in the / directory: [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata), [actions/checkout](https://github.com/actions/checkout) and [nick-fields/retry](https://github.com/nick-fields/retry).


Updates `dependabot/fetch-metadata` from 2.2.0 to 3.1.0
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/dbb049abf0d677abbd7f7eee0375145b417fdd34...25dd0e34f4fe68f24cc83900b1fe3fe149efef98)

Updates `actions/checkout` from 4.3.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/34e114876b0b11c390a56381ad16ebd13914f8d5...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `nick-fields/retry` from 3.0.2 to 4.0.0
- [Release notes](https://github.com/nick-fields/retry/releases)
- [Commits](https://github.com/nick-fields/retry/compare/ce71cc2ab81d554ebbe88c79ab5975992d79ba08...ad984534de44a9489a53aefd81eb77f87c70dc60)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: dependabot/fetch-metadata
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: nick-fields/retry
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 06:26:30 +00:00
Joel Male 28a72581fc chore: pin GitHub Actions to full commit SHAs
chore: pin GitHub Actions to full commit SHAs
2026-05-27 16:24:23 +10:00
dependabot[bot] ddcf38dab0 chore(deps-dev): bump pestphp/pest from 3.x-dev to 4.7.0
Bumps [pestphp/pest](https://github.com/pestphp/pest) from 3.x-dev to 4.7.0.
- [Release notes](https://github.com/pestphp/pest/releases)
- [Changelog](https://github.com/pestphp/pest/blob/4.x/RELEASE.md)
- [Commits](https://github.com/pestphp/pest/commits/v4.7.0)

---
updated-dependencies:
- dependency-name: pestphp/pest
  dependency-version: 4.7.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 06:19:38 +00:00
dependabot[bot] e7bf1ba3a3 chore(deps-dev): bump laravel/pint in the php-minor-patch group
Bumps the php-minor-patch group with 1 update: [laravel/pint](https://github.com/laravel/pint).


Updates `laravel/pint` from 1.21.0 to 1.29.1
- [Release notes](https://github.com/laravel/pint/releases)
- [Changelog](https://github.com/laravel/pint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/laravel/pint/compare/v1.21.0...v1.29.1)

---
updated-dependencies:
- dependency-name: laravel/pint
  dependency-version: 1.29.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: php-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 06:19:22 +00:00
pixel-workflows[bot] ebf5a41e01 chore: update dependabot configuration 2026-05-27 06:18:28 +00:00
Joel Male e0d566cebd chore: pin GitHub Actions to full commit SHAs
Pins all GitHub Actions uses: references to full commit SHAs
to prevent supply chain attacks via mutable action tags.
Original tag references are kept as inline comments.
2026-05-27 14:46:49 +10:00
Joel Male 863ef52e8d Revise security policy and reporting guidelines
Removed supported versions section and updated reporting instructions.
2026-05-27 14:46:49 +10:00
Joel Male 6638f812c8 chore: bump version to 2.1.0 2026-03-24 14:06:08 +10:00
Joel Male 8f442f51e0 chore: migrate to wearepixel/laravel-cart 2026-03-24 14:04:48 +10:00
Joel Male 6dfd0ce761 chore: bump version to 2.0.0 2026-03-23 16:30:38 +10:00
Joel Male a3161888aa chore: bump to v2.0.0 with PHP 8.3+ support and invalid item cleanup 2026-03-23 16:28:53 +10:00
Joel Male 4c02310bf7 feat: rough session re-implementation 2025-04-14 16:53:35 +10:00
Joel Male e810eb4ef4 Merge pull request #2 from joelwmale/feature/database-support
Database Support
2025-02-25 16:55:44 +10:00
Joel Male 4cf078277d feat: finish tests & docs 2025-02-25 16:54:03 +10:00
Joel Male 7adf5ecd1f feat: add tests and fix item level conditions for database storage 2025-02-25 16:31:42 +10:00
Joel Male 158cf38f14 feat: update composer deps & version 2025-02-25 16:08:27 +10:00
Joel Male 0f07b9ae8a feat: database support 2025-02-25 16:03:17 +10:00
Joel Male ec99aa0563 feat: clean up and starting documentation 2025-02-25 14:53:49 +10:00
Joel Male 8d6e1bcc26 wip: readme updates 2025-02-25 13:49:09 +10:00
Joel Male b92dce9ca4 wip: initial working database support 2025-02-25 11:47:10 +10:00
Joel Male 2196f58f01 Merge pull request #1 from joelwmale/feature/laravel-12.x-support
Laravel 12.x Support
2025-02-25 08:57:01 +10:00
Joel Male ed4d112665 Laravel 12.x Support 2025-02-25 08:54:04 +10:00
Joel Male f60bf92157 1.0.12 2024-11-28 13:11:23 +10:00
Joel Male a231ed8c43 1.0.12 2024-11-28 13:06:30 +10:00
Joel Male 599051631c wip: rename package to joelwmale 2024-11-28 13:06:24 +10:00
Joel Male b3ef4c38fc 1.0.11 2024-09-23 07:48:27 +10:00
Joel Male a45121ca63 Merge pull request #20 from wearepixel/task/fix-subtotal-always-being-used-if-the-total-is-$0
Task: Fix subtotal always being used when cart conditions on the total bring it to $0
2024-09-23 07:48:06 +10:00
Joel Male 5505f121c7 fix subtotal always being used 2024-09-23 07:45:59 +10:00
30 changed files with 4982 additions and 1853 deletions
+21 -5
View File
@@ -1,17 +1,33 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
target-branch: "master"
schedule: schedule:
interval: "weekly" interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
groups:
actions:
patterns: ["*"]
commit-message:
prefix: "chore"
include: "scope"
labels: labels:
- "deps" - "dependencies"
- package-ecosystem: "composer" - package-ecosystem: "composer"
directory: "/" directory: "/"
target-branch: "master"
schedule: schedule:
interval: "weekly" interval: "weekly"
day: "tuesday"
open-pull-requests-limit: 10
groups:
php-minor-patch:
update-types:
- "minor"
- "patch"
commit-message:
prefix: "chore"
include: "scope"
labels: labels:
- "deps" - "dependencies"
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Dependabot metadata - name: Dependabot metadata
id: metadata id: metadata
uses: dependabot/fetch-metadata@v2.2.0 uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with: with:
github-token: "${{ secrets.GITHUB_TOKEN }}" github-token: "${{ secrets.GITHUB_TOKEN }}"
+5 -6
View File
@@ -9,19 +9,19 @@ on:
jobs: jobs:
tests: tests:
name: Tests name: Tests
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
php: ['8.1', '8.2', '8.3'] php: ["8.3", "8.4", "8.5"]
stability: [prefer-stable] stability: [prefer-stable]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached
@@ -29,12 +29,11 @@ jobs:
coverage: none coverage: none
- name: Install dependencies - name: Install dependencies
uses: nick-fields/retry@v3 uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with: with:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }} command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }}
- name: Execute tests - name: Execute tests
run: vendor/bin/pest run: vendor/bin/pest
+1
View File
@@ -4,3 +4,4 @@
.phpunit.result.cache .phpunit.result.cache
READMETODO.md READMETODO.md
build build
docs/superpowers/
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) Pixel Digital Pty Ltd <hello@wearepixel.com.au> Copyright (c) Joel Male <joel@joelmale.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+251 -81
View File
@@ -1,41 +1,74 @@
<a name="top"></a>
[![Laravel Cart](./docs/laravel-cart.png)](https://joelmale.com)
[![Latest Version on Packagist](https://img.shields.io/packagist/v/wearepixel/laravel-cart.svg?style=flat-square)](https://packagist.org/packages/wearepixel/laravel-cart) [![Latest Version on Packagist](https://img.shields.io/packagist/v/wearepixel/laravel-cart.svg?style=flat-square)](https://packagist.org/packages/wearepixel/laravel-cart)
[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/wearepixel/laravel-cart/tests.yml?branch=master&label=Tests)](https://github.com/wearepixel/laravel-cart/actions?query=workflow%3ATests+branch%3Amaster) [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/wearepixel/laravel-cart/tests.yml?branch=master&label=Tests)](https://github.com/wearepixel/laravel-cart/actions?query=workflow%3ATests+branch%3Amaster)
[![Total Downloads](https://img.shields.io/packagist/dt/wearepixel/laravel-cart.svg?style=flat-square)](https://packagist.org/packages/wearepixel/laravel-cart) [![Total Downloads](https://img.shields.io/packagist/dt/wearepixel/laravel-cart.svg?style=flat-square)](https://packagist.org/packages/wearepixel/laravel-cart)
[![GitHub last commit](https://img.shields.io/github/last-commit/wearepixel/laravel-cart)](#)
[![License](https://poser.pugx.org/wearepixel/laravel-cart/license.svg)](https://packagist.org/packages/wearepixel/laravel-cart) [![License](https://poser.pugx.org/wearepixel/laravel-cart/license.svg)](https://packagist.org/packages/wearepixel/laravel-cart)
[![Free](https://img.shields.io/badge/free_for_non_commercial_use-brightgreen)](#-license)
A Cart Implementation for Laravel. A Cart Implementation for Laravel.
Supported Laravel Versions: 9, 10, 11 Supported Laravel Versions: 10, 11, 12, and 13.
## Quick Demo For Laravel 9.0 and below, please use version [1.0](https://github.com/wearepixel/laravel-cart/releases/tag/1.0.12)
```php ## Table of Contents
// Add an item to the cart
\Cart::add(1, 'Product 1', 19.99, 2, ['size' => 'large']);
// Get the cart contents - [Getting Started](#-getting-started)
$cartContents = \Cart::getContent(); - [Demo](#-deni)
- [Documentation](#-documentation)
- [Configuration](#configuration)
- [Basic Usage](#basic-usage)
- [Conditions](#conditions)
- [Cart Items](#cart-items)
- [Database Support](#database-support)
- [Events](#events)
- [Credits](#-credits)
- [License](#-license)
// Update item quantity by its id ## 🚀 Getting Started
\Cart::update(1, ['quantity' => 3]);
// Remove an item from the cart by its id ### 🔥 Installing
\Cart::remove(1);
// Get cart total
\$total = Cart::getTotal();
// Clear the entire cart
\Cart::clear();
```
## Installing
Install the package through [Composer](http://getcomposer.org/). Install the package through [Composer](http://getcomposer.org/).
`composer require "wearepixel/laravel-cart"` `composer require wearepixel/laravel-cart`
## Configuration ## 🧑‍🍳 Demo
```php
// Add an item to the cart
\Cart::add(
1, // any unique id
'Product 1', // product name
19.99, // product price
2, // quantity
['size' => 'large'] // an array of extra attributes
);
// get the entire cart
$cartContents = \Cart::getContent();
// update an item already in the cart
\Cart::update(
1, // the same unique id that was used to add the item
['quantity' => 3] // the quantity to update
);
// remove the item by its id
\Cart::remove(1);
// get the total of the cart
$total = Cart::getTotal();
// clear it all when you've finished (like when you've stored the order)
\Cart::clear();
```
## 📚 Documentation
### ⚙️ Configuration
You can publish the configuration file to customize various options. You can publish the configuration file to customize various options.
@@ -43,7 +76,7 @@ ## Configuration
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag="config" php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag="config"
``` ```
### Formatting Numbers #### Formatting Numbers
The package by default does not use round to format numbers, and instead returns the number using floatval(). The package by default does not use round to format numbers, and instead returns the number using floatval().
@@ -56,7 +89,7 @@ ### Formatting Numbers
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'), 'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
``` ```
### Decimals #### Decimals
You can customize the number of decimals in the configuration file. You can customize the number of decimals in the configuration file.
@@ -66,7 +99,7 @@ ### Decimals
'decimals' => env('LARAVEL_CART_DECIMALS', 2), 'decimals' => env('LARAVEL_CART_DECIMALS', 2),
``` ```
### Round Mode #### Round Mode
The package uses the `round` function to round the prices. You can customize the rounding mode in the configuration file. The package uses the `round` function to round the prices. You can customize the rounding mode in the configuration file.
@@ -76,40 +109,22 @@ ### Round Mode
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'), 'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
``` ```
## Usage ### Basic Usage
- [Basic Usage](#basic-usage) The cart has a default sessionKey that holds the cart data and stores it in the session, so you can have multiple carts for multiple users.
- [Conditions](#conditions)
- [Items](#items)
- [Associating Models](#associating-models)
- [Instances](#instances)
- [Exceptions](#exceptions)
- [Events](#events)
- [Format Response](#format)
- [Examples](#examples)
- [Using Different Storage](#storage)
- [License](#license)
## Usage This also serves as a cart unique identifier which you can use to bind a cart to a specific user if you want to.
The cart has a default sessionKey that holds the cart data and stores it in the session. Make sure to call `\Cart::setSessionKey($sessionKey)` before calling any other cart methods.
This
also serves as a cart unique identifier which you can use to bind a cart to a specific user if you want to.
Make sure to call `\Cart::session($sessionKey)` before calling any other cart methods.
Usually this is not required. Usually this is not required.
```php ```php
// Binds the cart to a unique id (user id, session id, etc.) // Binds the cart to a unique id (user id, session id, etc.)
\Cart::session(User::first()->id); \Cart::setSessionKey(User::first()->id);
// If you don't want to bind the cart on a project level, you can add it to each call
\Cart::session(User::first()->id)->add(1, 'Product 1', 19.99, 2, ['size' => 'large']);
``` ```
### Adding to the cart: **Cart::add()** #### Adding to the cart: **Cart::add()**
There are a few ways to add items to the cart. There are a few ways to add items to the cart.
@@ -180,7 +195,7 @@ ### Adding to the cart: **Cart::add()**
); );
``` ```
### Updating an item on a cart: **Cart::update()** #### Updating an item on a cart: **Cart::update()**
```php ```php
Cart::update( Cart::update(
@@ -219,14 +234,14 @@ ### Updating an item on a cart: **Cart::update()**
); );
``` ```
### Removing an item on a cart: **Cart::remove()** #### Removing an item on a cart: **Cart::remove()**
```php ```php
// Remove an item from the cart by its id // Remove an item from the cart by its id
Cart::remove(456); Cart::remove(456);
``` ```
### Getting an item on a cart: **Cart::get()** #### Getting an item on a cart: **Cart::get()**
```php ```php
// Get an item from the cart by its id // Get an item from the cart by its id
@@ -236,7 +251,7 @@ ### Getting an item on a cart: **Cart::get()**
$summedPrice = Cart::get($itemId)->getPriceSum(); $summedPrice = Cart::get($itemId)->getPriceSum();
``` ```
### Getting the cart content: **Cart::getContent()** #### Getting the cart content: **Cart::getContent()**
```php ```php
// Returns a collection of the cart's contents // Returns a collection of the cart's contents
@@ -256,13 +271,13 @@ ### Getting the cart content: **Cart::getContent()**
$cartTotalQuantity = Cart::getTotalQuantity(); $cartTotalQuantity = Cart::getTotalQuantity();
``` ```
### Cart subtotal: **Cart::getSubTotal()** #### Cart subtotal: **Cart::getSubTotal()**
```php ```php
$subTotal = Cart::getSubTotal(); $subTotal = Cart::getSubTotal();
``` ```
### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()** #### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
```php ```php
$subTotalWithoutConditions = Cart::getSubTotalWithoutConditions(); $subTotalWithoutConditions = Cart::getSubTotalWithoutConditions();
@@ -274,19 +289,29 @@ ### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
$total = Cart::getTotal(); $total = Cart::getTotal();
``` ```
### Check if cart is empty: **Cart::isEmpty()** #### Check if cart is empty: **Cart::isEmpty()**
```php ```php
Cart::isEmpty(); Cart::isEmpty();
``` ```
### Clearing the Cart: **Cart::clear()** #### Clearing the Cart: **Cart::clear()**
This clears all items and conditions from the cart.
```php ```php
Cart::clear(); Cart::clear();
``` ```
## Conditions #### Clearing the cart items only: **Cart::clearItems()**
This clears all items, but keeps the conditions (useful for when you want to keep the conditions but remove the items)
```php
Cart::clearItems();
```
### Conditions
Conditions are very useful for adding things like discounts, taxes, shipping, etc. Conditions are very useful for adding things like discounts, taxes, shipping, etc.
@@ -298,9 +323,9 @@ ## Conditions
You can also provide an `order` to cart conditions which tells the cart in what order to apply the conditions. Item level conditions do not support the `order` parameter. You can also provide an `order` to cart conditions which tells the cart in what order to apply the conditions. Item level conditions do not support the `order` parameter.
### Conditions on the cart #### Conditions on the cart
#### Adding a condition to the cart: **Cart::condition()** ##### Adding a condition to the cart: **Cart::condition()**
```php ```php
// Add a single condition to the cart // Add a single condition to the cart
@@ -351,7 +376,7 @@ #### Adding a condition to the cart: **Cart::condition()**
Cart::condition($shipping); Cart::condition($shipping);
``` ```
#### Getting conditions on the cart: **Cart::getConditions()** ##### Getting conditions on the cart: **Cart::getConditions()**
```php ```php
// To get all applied conditions on a cart, use below: // To get all applied conditions on a cart, use below:
@@ -370,7 +395,7 @@ #### Getting conditions on the cart: **Cart::getConditions()**
} }
``` ```
#### Getting conditions on the cart as an array: **Cart::getConditions(array: true)** ##### Getting conditions on the cart as an array: **Cart::getConditions(array: true)**
You can get all cart conditions in array format by passing "array: true". This is useful if you want to store the carts conditions on a Livewire component since by default we have collections inside collections for conditions which Livewire does not support. You can get all cart conditions in array format by passing "array: true". This is useful if you want to store the carts conditions on a Livewire component since by default we have collections inside collections for conditions which Livewire does not support.
@@ -389,7 +414,7 @@ #### Getting conditions on the cart as an array: **Cart::getConditions(array: tr
} }
``` ```
#### Getting conditions by name: **Cart::getCondition($conditionName)** ##### Getting conditions by name: **Cart::getCondition($conditionName)**
```php ```php
$condition = Cart::getCondition('GST'); $condition = Cart::getCondition('GST');
@@ -403,7 +428,7 @@ #### Getting conditions by name: **Cart::getCondition($conditionName)**
$condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added $condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added
``` ```
#### Getting active conditions on the cart: **Cart::getConditions(active: true)** ##### Getting active conditions on the cart: **Cart::getConditions(active: true)**
You can get only active conditions by passsing `active: true` to the `getConditions()` method. You can get only active conditions by passsing `active: true` to the `getConditions()` method.
@@ -439,14 +464,14 @@ #### Getting active conditions on the cart: **Cart::getConditions(active: true)*
// will return no conditions if the subtotal is $210 // will return no conditions if the subtotal is $210
``` ```
### Calculating condition value ##### Calculating condition value
There are 2 ways to calculate the value of a condition: There are 2 ways to calculate the value of a condition:
1. Using the `getCalculatedValue` method on the condition instance 1. Using the `getCalculatedValue` method on the condition instance
2. Using the `getCalculatedValueForCondition` method on the cart instance and passing the condition name 2. Using the `getCalculatedValueForCondition` method on the cart instance and passing the condition name
#### Using the `getCalculatedValue` method on the condition instance ##### Using the `getCalculatedValue` method on the condition instance
```php ```php
$subTotal = Cart::getSubTotal(); $subTotal = Cart::getSubTotal();
@@ -454,7 +479,7 @@ #### Using the `getCalculatedValue` method on the condition instance
$conditionCalculatedValue = $condition->getCalculatedValue($subTotal); $conditionCalculatedValue = $condition->getCalculatedValue($subTotal);
``` ```
#### Using the `getCalculatedValueForCondition` method on the cart instance ##### Using the `getCalculatedValueForCondition` method on the cart instance
This method automatically calculates the value of a condition by it's name based on the order of the conditions. This method automatically calculates the value of a condition by it's name based on the order of the conditions.
@@ -487,7 +512,7 @@ #### Using the `getCalculatedValueForCondition` method on the cart instance
Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0 Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0
``` ```
### Adding conditions that activate once a minimum value is met ##### Adding conditions that activate once a minimum value is met
You can add a `minimum` amount required for a condition to activate. You can add a `minimum` amount required for a condition to activate.
@@ -506,7 +531,7 @@ ### Adding conditions that activate once a minimum value is met
Cart::condition($tenPercentOff) Cart::condition($tenPercentOff)
``` ```
### Adding conditions that only activate up to a maximum value ##### Adding conditions that only activate up to a maximum value
You can add a `maximum` amount required for a condition to activate. You can add a `maximum` amount required for a condition to activate.
@@ -525,7 +550,7 @@ ### Adding conditions that only activate up to a maximum value
Cart::condition($shipping) Cart::condition($shipping)
``` ```
### Condition on items #### Conditions on items
Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value. Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value.
@@ -586,7 +611,7 @@ ### Condition on items
$cartSubTotal = Cart::getSubTotal(); $cartSubTotal = Cart::getSubTotal();
``` ```
#### Add a condition to an existing item on the cart: **Cart::addItemCondition($productId, $itemCondition)** ##### Add a condition to an existing item on the cart: **Cart::addItemCondition($productId, $itemCondition)**
Adding Condition to an existing Item on the cart is simple as well. Adding Condition to an existing Item on the cart is simple as well.
@@ -600,7 +625,7 @@ #### Add a condition to an existing item on the cart: **Cart::addItemCondition($
Cart::addItemCondition(456, $condition); Cart::addItemCondition(456, $condition);
``` ```
### Clearing Cart Conditions: **Cart::clearCartConditions()** #### Clearing Cart Conditions: **Cart::clearCartConditions()**
This clears all cart level conditions, and does not affect item level conditions. This clears all cart level conditions, and does not affect item level conditions.
@@ -614,25 +639,25 @@ ### Clearing Cart Conditions: **Cart::clearCartConditions()**
Cart::clearAllConditions() Cart::clearAllConditions()
``` ```
### Remove a specific cart condtion: **Cart::removeCartCondition($conditionName)** ##### Remove a specific cart condtion: **Cart::removeCartCondition($conditionName)**
```php ```php
Cart::removeCartCondition('Summer Sale 5%') Cart::removeCartCondition('Summer Sale 5%')
``` ```
### Remove a specific item condition: **Cart::removeItemCondition($itemId, $conditionName)** ##### Remove a specific item condition: **Cart::removeItemCondition($itemId, $conditionName)**
```php ```php
Cart::removeItemCondition(456, 'SALE 5%') Cart::removeItemCondition(456, 'SALE 5%')
``` ```
### Clear all item conditions: **Cart::clearItemConditions($itemId)** ##### Clear all item conditions: **Cart::clearItemConditions($itemId)**
```php ```php
Cart::clearItemConditions(456) Cart::clearItemConditions(456)
``` ```
### Get conditions by type: **Cart::getConditionsByType($type)** #### Get conditions by type: **Cart::getConditionsByType($type)**
This returns all conditions that has been added to the cart by the type specified. This returns all conditions that has been added to the cart by the type specified.
@@ -640,13 +665,13 @@ ### Get conditions by type: **Cart::getConditionsByType($type)**
$tax = Cart::getConditionsByType('tax'); $tax = Cart::getConditionsByType('tax');
``` ```
### Remove conditions by type: **Cart::removeConditionsByType($type)** ##### Remove conditions by type: **Cart::removeConditionsByType($type)**
```php ```php
Cart::removeConditionsByType('tax'); Cart::removeConditionsByType('tax');
``` ```
## Cart Items ### Cart Items
The method **Cart::getContent()** returns a collection of items. The method **Cart::getContent()** returns a collection of items.
@@ -668,8 +693,153 @@ ## Cart Items
$item->getPriceSumWithConditions(); $item->getPriceSumWithConditions();
``` ```
## Credits ### Storage Options
Huge credits to the original maintainer of the package, located at [darryldecode/laravelshoppingcart](https://github.com/darryldecode/laravelshoppingcart) By default the cart is stored in the session, but there are times you may want to store the cart in the database.
We have forked this package to update it to Laravel 11 and to add some features and support for newer PHP versions. 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.
#### 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'); // 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 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.
## 📓 License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
+11
View File
@@ -0,0 +1,11 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Reporting a Vulnerability
If you discover a security vulnerability, please report it privately using the below channel:
**GitHub Private Vulnerability Reporting** — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
All security vulnerabilities will be promptly addressed.
+17 -10
View File
@@ -1,6 +1,6 @@
{ {
"name": "wearepixel/laravel-cart", "name": "wearepixel/laravel-cart",
"version": "1.0.10", "version": "2.1.4",
"description": "A cart implementation for Laravel", "description": "A cart implementation for Laravel",
"keywords": [ "keywords": [
"laravel", "laravel",
@@ -15,26 +15,33 @@
}, },
{ {
"name": "Joel Male", "name": "Joel Male",
"email": "joel@wearepixel.com.au" "email": "joel@joelmale.com"
} }
], ],
"require": { "require": {
"php": ">=8.1", "php": ">=8.3",
"illuminate/support": "^9.0|^10.0|^11.0", "illuminate/support": "^11.0|^12.0|^13.0",
"illuminate/validation": "^9.0|^10.0|^11.0", "illuminate/auth": "^11.0|^12.0|^13.0",
"illuminate/translation": "^9.0|^10.0|^11.0" "illuminate/container": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/validation": "^11.0|^12.0|^13.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^1.4.2", "mockery/mockery": "^2.0",
"laravel/pint": "^1.17", "laravel/pint": "^1.21",
"pestphp/pest": "2.35.1", "pestphp/pest": "^4.7",
"larapack/dd": "1.1" "larapack/dd": "dev-master"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Wearepixel\\Cart\\": "src/" "Wearepixel\\Cart\\": "src/"
} }
}, },
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev", "minimum-stability": "dev",
"extra": { "extra": {
"laravel": { "laravel": {
Generated
+3996 -1566
View File
File diff suppressed because it is too large Load Diff
+11 -8
View File
@@ -14,15 +14,18 @@
/* /*
* --------------------------------------------------------------- * ---------------------------------------------------------------
* Persistence * Storage
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
'storage' => null, 'driver' => 'session',
/* 'storage' => [
* --------------------------------------------------------------- 'session',
* Events 'database' => [
* --------------------------------------------------------------- 'model' => '',
*/ 'id' => '',
'events' => null, 'items' => '',
'conditions' => '',
],
],
]; ];
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+6 -7
View File
@@ -1,17 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd">
bootstrap="vendor/autoload.php" <source ignoreIndirectDeprecations="true">
colors="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd"
>
<source>
<include> <include>
<directory suffix=".php">./src</directory> <directory suffix=".php">./src</directory>
</include> </include>
</source> </source>
<testsuites> <testsuites>
<testsuite name="Laravel Cart Test Suite"> <testsuite name="Laravel Cart Test Suite">
<directory>tests</directory> <directory>tests</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
</php>
</phpunit> </phpunit>
+113 -81
View File
@@ -15,10 +15,6 @@ class Cart
protected $instanceName; protected $instanceName;
protected $sessionKey;
protected $sessionKeyCartItems;
protected $sessionKeyCartConditions;
protected $config; protected $config;
protected $currentItemId; protected $currentItemId;
@@ -26,39 +22,48 @@ class Cart
protected $decimals; protected $decimals;
protected $decPoint; protected $decPoint;
public function __construct($session, $events, $instanceName, $session_key, $config) public function __construct($sessionStorage, $events, $instanceName, $sessionKey, $config)
{ {
$this->validateConfig($config);
$this->events = $events; $this->events = $events;
$this->session = $session; $this->session = new CartSession($sessionStorage, $sessionKey, $config);
$this->instanceName = $instanceName; $this->instanceName = $instanceName;
$this->sessionKey = $session_key;
$this->sessionKeyCartItems = $this->sessionKey . '_cart_items';
$this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions';
$this->config = $config; $this->config = $config;
$this->currentItemId = null; $this->currentItemId = null;
$this->fireEvent('created'); $this->fireEvent('Created');
} }
/** private function validateConfig($config)
* sets the session key
*
* @param string $sessionKey the session key or identifier
* @return $this|bool
*
* @throws \Exception
*/
public function session($sessionKey)
{ {
if (! $sessionKey) { if ($config['driver'] == 'database') {
throw new \Exception('Session key is required.'); if (! isset($config['storage']['database']['model'])) {
throw new \Exception('You need to set a model for the database driver in the cart configuration.');
}
if (! isset($config['storage']['database']['id'])) {
throw new \Exception('You need to set an id (for the session) for the database driver in the cart configuration.');
}
if (! isset($config['storage']['database']['items'])) {
throw new \Exception('You need to set an items key for the database driver in the cart configuration.');
}
if (! isset($config['storage']['database']['conditions'])) {
throw new \Exception('You need to set a conditions key for the database driver in the cart configuration.');
}
} }
}
$this->sessionKey = $sessionKey; public function getSessionKey(): string
$this->sessionKeyCartItems = $this->sessionKey . '_cart_items'; {
$this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions'; return $this->session->getSessionKey();
}
return $this; public function setSessionKey(string $sessionKey): void
{
$this->session->setSessionKey($sessionKey);
} }
/** /**
@@ -160,15 +165,37 @@ public function add(
$this->currentItemId = $id; $this->currentItemId = $id;
// clean up any incomplete items
$this->removeInvalidItems();
return $this; return $this;
} }
/**
* Remove any incomplete items
*/
public function removeInvalidItems(): bool
{
$carts = $this->getContent();
foreach ($carts as $index => $cart) {
if (! (isset($cart['id'], $cart['name'], $cart['quantity']))) {
// remove item
$this->remove($index);
}
}
return true;
}
/** /**
* Update a cart item with whatever data is passed in * Update a cart item with whatever data is passed in
*/ */
public function update(int|string $id, array $data): bool public function update(int|string $id, array $data): bool
{ {
if ($this->fireEvent('updating', $data) === false) { if ($this->fireEvent('Updating', 'item', $data) === false) {
return false; return false;
} }
@@ -201,7 +228,7 @@ public function update(int|string $id, array $data): bool
$this->save($cart); $this->save($cart);
$this->fireEvent('updated', $item); $this->fireEvent('Updated', 'item', $item);
return true; return true;
} }
@@ -260,7 +287,7 @@ public function remove($id)
{ {
$cart = $this->getContent(); $cart = $this->getContent();
if ($this->fireEvent('removing', $id) === false) { if ($this->fireEvent('Removing', 'item', $id) === false) {
return false; return false;
} }
@@ -268,28 +295,33 @@ public function remove($id)
$this->save($cart); $this->save($cart);
$this->fireEvent('removed', $id); $this->fireEvent('Removed', 'item', $id);
return true; return true;
} }
/**
* clear cart
*
* @return bool
*/
public function clear() public function clear()
{ {
if ($this->fireEvent('clearing') === false) { if ($this->fireEvent('Clearing') === false) {
return false; return false;
} }
$this->session->put( $this->session->clear();
$this->sessionKeyCartItems,
[]
);
$this->fireEvent('cleared'); $this->fireEvent('Cleared');
return true;
}
public function clearItems()
{
if ($this->fireEvent('Clearing') === false) {
return false;
}
$this->session->clearItems();
$this->fireEvent('Cleared');
return true; return true;
} }
@@ -326,7 +358,9 @@ public function condition(array|CartCondition $condition): self
$conditions->put($condition->getName(), $condition); $conditions->put($condition->getName(), $condition);
$conditions = $conditions->sortBy(function ($condition, $key) { $conditions = $conditions->reject(function ($condition) {
return empty($condition);
})->sortBy(function ($condition, $key) {
return $condition->getOrder(); return $condition->getOrder();
}); });
@@ -342,9 +376,14 @@ public function condition(array|CartCondition $condition): self
*/ */
public function getConditions(bool $array = false, bool $active = false): CartConditionCollection|array public function getConditions(bool $array = false, bool $active = false): CartConditionCollection|array
{ {
$conditions = new CartConditionCollection( $conditions = new CartConditionCollection($this->session->getConditions());
$this->session->get($this->sessionKeyCartConditions)
); $conditions = $conditions->reject(function ($condition) {
return empty($condition);
})
->transform(function ($condition) {
return $condition instanceof CartCondition ? $condition : new CartCondition($condition);
});
if ($active) { if ($active) {
return $conditions->filter(function (CartCondition $condition) { return $conditions->filter(function (CartCondition $condition) {
@@ -458,9 +497,6 @@ public function removeItemCondition($itemId, $conditionName)
} }
} }
// reset indexes
$item['conditions'] = array_values($item['conditions']);
$this->update($itemId, [ $this->update($itemId, [
'conditions' => $item['conditions'], 'conditions' => $item['conditions'],
]); ]);
@@ -491,10 +527,7 @@ public function clearItemConditions($itemId)
*/ */
public function clearCartConditions(): void public function clearCartConditions(): void
{ {
$this->session->put( $this->session->putConditions([]);
$this->sessionKeyCartConditions,
[]
);
} }
/** /**
@@ -528,14 +561,10 @@ public function getSubTotalWithoutConditions($formatted = true)
return Helpers::formatValue(floatval($sum), $formatted, $this->config); return Helpers::formatValue(floatval($sum), $formatted, $this->config);
} }
/**
* get cart sub total
*
* @param bool $formatted
*/
public function getSubTotal($formatted = true): int|float|string public function getSubTotal($formatted = true): int|float|string
{ {
$subTotal = 0.00; $subTotal = 0.00;
// add all the items together with conditions applied // add all the items together with conditions applied
$subTotalSum = $this->getContent()->sum(function (ItemCollection $item) { $subTotalSum = $this->getContent()->sum(function (ItemCollection $item) {
return $item->getPriceSumWithConditions(false); return $item->getPriceSumWithConditions(false);
@@ -601,10 +630,14 @@ public function getTotal(): int|float
return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config); return Helpers::formatValue($subTotal, $this->config['format_numbers'], $this->config);
} }
$conditionsForTotal->each(function (CartCondition $condition) use ($subTotal, &$total) { $index = 0;
$toBeCalculated = $total > 0 ? $total : $subTotal;
$conditionsForTotal->each(function (CartCondition $condition) use ($subTotal, &$total, &$index) {
$toBeCalculated = ($index > 0) ? $total : $subTotal;
$total = $condition->applyCondition($toBeCalculated); $total = $condition->applyCondition($toBeCalculated);
$index++;
}); });
return Helpers::formatValue($total, $this->config['format_numbers'], $this->config); return Helpers::formatValue($total, $this->config['format_numbers'], $this->config);
@@ -652,24 +685,16 @@ public function getTotalQuantity()
return $count; return $count;
} }
/** public function getContent(): CartCollection
* get the cart
*
* @return CartCollection
*/
public function getContent()
{ {
return (new CartCollection($this->session->get($this->sessionKeyCartItems)))->reject(function ($item) { return (new CartCollection($this->session->getItems()))->transform(function ($item) {
return $item instanceof ItemCollection ? $item : new ItemCollection($item, $this->config);
})->reject(function ($item) {
return ! ($item instanceof ItemCollection); return ! ($item instanceof ItemCollection);
}); });
} }
/** public function isEmpty($test = null)
* check if cart is empty
*
* @return bool
*/
public function isEmpty()
{ {
return $this->getContent()->isEmpty(); return $this->getContent()->isEmpty();
} }
@@ -715,7 +740,7 @@ protected function validate($item)
*/ */
protected function addItem($id, $item) protected function addItem($id, $item)
{ {
if ($this->fireEvent('adding', $item) === false) { if ($this->fireEvent('Adding', 'item', $item) === false) {
return false; return false;
} }
@@ -725,7 +750,7 @@ protected function addItem($id, $item)
$this->save($cart); $this->save($cart);
$this->fireEvent('added', $item); $this->fireEvent('Added', 'item', $item);
return true; return true;
} }
@@ -733,11 +758,11 @@ protected function addItem($id, $item)
/** /**
* save the cart * save the cart
* *
* @param $cart CartCollection * @param $cart CartCollection
*/ */
protected function save($cart) protected function save($cart)
{ {
$this->session->put($this->sessionKeyCartItems, $cart); $this->session->putItems($cart);
} }
/** /**
@@ -745,7 +770,7 @@ protected function save($cart)
*/ */
protected function saveConditions($conditions) protected function saveConditions($conditions)
{ {
$this->session->put($this->sessionKeyCartConditions, $conditions); $this->session->putConditions($conditions);
} }
/** /**
@@ -816,12 +841,19 @@ public function setDecimals($decimals)
$this->decimals = $decimals; $this->decimals = $decimals;
} }
/** protected function fireEvent($name, $type = null, $data = null)
* @return mixed
*/
protected function fireEvent($name, $value = [])
{ {
return $this->events->dispatch($this->getInstanceName() . '.' . $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);
} }
/** /**
+8 -13
View File
@@ -35,24 +35,19 @@ public function register()
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'cart'); $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'cart');
$this->app->singleton('cart', function ($app) { $this->app->singleton('cart', function ($app) {
$storageClass = config('cart.storage'); $config = config('cart');
$eventsClass = config('cart.events'); $events = $app['events'];
$storage = $storageClass ? new $storageClass : $app['session']; $storage = $config['driver'] === 'database'
$events = $eventsClass ? new $eventsClass : $app['events']; ? new $config['storage']['database']['model']
$instanceName = 'cart'; : $app['session'];
// default session or cart identifier. This will be overridden when calling Cart::session($sessionKey)->add() etc..
// like when adding a cart for a specific user name. Session Key can be string or maybe a unique identifier to bind a cart
// to a specific user, this can also be a user ID
$session_key = '4yTlTDKu3oJOfzD';
return new Cart( return new Cart(
$storage, $storage,
$events, $events,
$instanceName, 'cart',
$session_key, session()->getId(),
config('cart') $config
); );
}); });
} }
+172
View File
@@ -0,0 +1,172 @@
<?php
namespace Wearepixel\Cart;
class CartSession
{
private $customStorage;
public $sessionKey;
public $driver;
private $session;
private $sessionId = null;
private $itemsKey = null;
private $conditionsKey = null;
public function __construct($session, $sessionKey, $config)
{
$this->sessionKey = $sessionKey;
$this->driver = $config['driver'];
if ($this->driver === 'session') {
$this->customStorage = false;
$this->session = $session;
$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'];
$this->itemsKey = $config['storage']['database']['items'];
$this->conditionsKey = $config['storage']['database']['conditions'];
$this->session = $session->firstOrNew([$this->sessionId => $this->sessionKey]);
$this->session[$this->itemsKey] = $this->session[$this->itemsKey] ?? [];
$this->session[$this->conditionsKey] = $this->session[$this->conditionsKey] ?? [];
}
}
public function isInternalSession($session)
{
return in_array(is_object($session) ? get_class($session) : $session, [
'Illuminate\Session\SessionManager',
'Wearepixel\Cart\Tests\Helpers\SessionMock',
]);
}
public function getSessionKey()
{
return $this->sessionKey;
}
public function setSessionKey(string $sessionKey)
{
// set the session key and then update the session
$this->sessionKey = $sessionKey;
if ($this->driver == 'session') {
$session = $this->session->get($this->itemsKey);
$sessionKey = $this->session->get($this->conditionsKey);
$this->session->forget($this->itemsKey);
$this->session->forget($this->conditionsKey);
$this->itemsKey = $this->sessionKey . '_cart_items';
$this->conditionsKey = $this->sessionKey . '_cart_conditions';
$this->session->put($this->itemsKey, $session);
$this->session->put($this->conditionsKey, $sessionKey);
} elseif ($this->driver == 'database') {
$this->session[$this->sessionId] = $sessionKey;
$this->session->save();
}
}
public function has($key)
{
return $this->session->has($key) ?? $this->session;
}
public function getItems()
{
return $this->driver == 'session' ? $this->session->get($this->itemsKey) : $this->session[$this->itemsKey];
}
public function getConditions()
{
return $this->driver == 'session' ? $this->session->get($this->conditionsKey) : $this->session[$this->conditionsKey];
}
public function putItems($value)
{
if ($this->driver == 'session') {
return $this->session->put($this->itemsKey, $value);
}
// convert any conditions to arrays
$value = is_array($value) ? $value : $value->toArray();
foreach ($value as $key => $item) {
if ($item['conditions']) {
if (is_array($item['conditions'])) {
$value[$key]['conditions'] = collect($item['conditions'])->map(function ($condition) {
return $condition->toArray();
});
} else {
$value[$key]['conditions'] = $item['conditions']->toArray();
}
}
}
$this->session[$this->itemsKey] = $value;
$this->session->save();
return $this->session;
}
public function putConditions($value)
{
if ($this->driver == 'session') {
return $this->session->put($this->conditionsKey, $value);
}
if (! empty($value)) {
$value = $value->mapWithKeys(function ($condition) {
return [$condition->getName() => $condition->toArray()];
});
}
$this->session[$this->conditionsKey] = $value;
$this->session->save();
return $this->session;
}
public function clearItems()
{
if ($this->driver == 'session') {
$this->session->forget($this->itemsKey);
return true;
}
$this->session[$this->itemsKey] = [];
return $this->session->save();
}
public function clear()
{
if ($this->driver == 'session') {
$this->session->forget($this->itemsKey);
$this->session->forget($this->conditionsKey);
return true;
}
$deleted = $this->session->delete();
$this->session[$this->itemsKey] = [];
$this->session[$this->conditionsKey] = [];
return $deleted;
}
public function getSessionModel()
{
return $this->session;
}
}
+31 -1
View File
@@ -3,10 +3,40 @@
namespace Wearepixel\Cart\Facades; namespace Wearepixel\Cart\Facades;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use Wearepixel\Cart\Cart;
use Wearepixel\Cart\CartCollection;
use Wearepixel\Cart\CartCondition;
use Wearepixel\Cart\CartConditionCollection;
use Wearepixel\Cart\ItemCollection;
/**
* @mixin Cart
*
* @method static ItemCollection|null get(int|string $itemId)
* @method static bool has(int|string $itemId)
* @method static self add(string|int|array<string, mixed> $id, string|null $name = null, float|null $price = null, int|float|null $quantity = null, array<string, mixed> $attributes = [], CartCondition|array<mixed> $conditions = [], string|null $associatedModel = null)
* @method static bool update(int|string $id, array $data)
* @method static void remove(int|string $id)
* @method static void clear()
* @method static void clearItems()
* @method static Cart condition(array|CartCondition $condition)
* @method static CartConditionCollection|array getConditions(bool $array = false, bool $active = false)
* @method static CartCondition|null getCondition(string $conditionName)
* @method static CartConditionCollection getConditionsByType(string $type)
* @method static void removeConditionsByType(string $type)
* @method static void removeCartCondition(string $conditionName)
* @method static void clearItemConditions(int|string $itemId)
* @method static void clearAllConditions()
* @method static CartCollection getContent()
* @method static bool isEmpty()
* @method static int getTotalQuantity()
* @method static int|float|string getSubTotal(bool $formatted = true)
* @method static int|float|string getSubTotalWithoutConditions(bool $formatted = true)
* @method static int|float getTotal()
*/
class CartFacade extends Facade class CartFacade extends Facade
{ {
protected static function getFacadeAccessor() protected static function getFacadeAccessor(): string
{ {
return 'cart'; return 'cart';
} }
+34 -4
View File
@@ -91,7 +91,37 @@ public function getConditions()
return []; return [];
} }
return $this['conditions']; $conditionsArray = [];
$conditions = $this['conditions'];
// check if we're already an array of CartConditions
foreach ($conditions as $key => $condition) {
if ($condition instanceof CartCondition) {
return $conditions;
}
}
$hasSubArray = false;
foreach ($conditions as $key => $condition) {
if (is_array($condition) && ! $condition instanceof CartCondition) {
$hasSubArray = true;
$conditionsArray[] = new CartCondition($condition);
} elseif ($condition instanceof CartCondition) {
$conditionsArray[] = $condition;
}
}
if (! $hasSubArray) {
if ($conditions instanceof CartCondition) {
return $conditions;
}
$conditionsArray[] = new CartCondition($conditions);
}
return $conditionsArray;
} }
/** /**
@@ -108,14 +138,14 @@ public function getPriceWithConditions($formatted = true)
$processed = 0; $processed = 0;
if ($this->hasConditions()) { if ($this->hasConditions()) {
if (is_array($this->conditions)) { if (is_array($this->getConditions())) {
foreach ($this->conditions as $condition) { foreach ($this->getConditions() as $condition) {
($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice; ($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice;
$newPrice = $condition->applyCondition($toBeCalculated); $newPrice = $condition->applyCondition($toBeCalculated);
$processed++; $processed++;
} }
} else { } else {
$newPrice = $this['conditions']->applyCondition($originalPrice); $newPrice = $this->getConditions()->applyCondition($originalPrice);
} }
return Helpers::formatValue($newPrice, $formatted, $this->config); return Helpers::formatValue($newPrice, $formatted, $this->config);
+1 -2
View File
@@ -4,6 +4,5 @@
'format_numbers' => true, 'format_numbers' => true,
'decimals' => 2, 'decimals' => 2,
'dec_point' => '.', 'dec_point' => '.',
'thousands_sep' => ',', 'driver' => 'session',
'use_cents' => false,
]; ];
+17
View File
@@ -0,0 +1,17 @@
<?php
return [
'format_numbers' => true,
'decimals' => 2,
'round_mode' => 'up',
'driver' => 'database',
'storage' => [
'session',
'database' => [
'model' => \Wearepixel\Cart\Tests\Helpers\MockCartModel::class,
'id' => 'session_id',
'items' => 'items',
'conditions' => 'conditions',
],
],
];
+1
View File
@@ -4,4 +4,5 @@
'format_numbers' => true, 'format_numbers' => true,
'decimals' => 2, 'decimals' => 2,
'round_mode' => 'up', 'round_mode' => 'up',
'driver' => 'session',
]; ];
+1 -1
View File
@@ -4,5 +4,5 @@
'format_numbers' => false, 'format_numbers' => false,
'decimals' => 3, 'decimals' => 3,
'dec_point' => ',', 'dec_point' => ',',
'thousands_sep' => '.', 'driver' => 'session',
]; ];
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace Wearepixel\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';
}
+5
View File
@@ -20,4 +20,9 @@ public function put($key, $value)
{ {
$this->session[$key] = $value; $this->session[$key] = $value;
} }
public function forget($key)
{
unset($this->session[$key]);
}
} }
+2 -26
View File
@@ -1,29 +1,5 @@
<?php <?php
/* use Tests\TestCase;
|--------------------------------------------------------------------------
| 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.
|
*/
// pest()->extend(Tests\TestCase::class)->in('Feature'); uses(TestCase::class)->in(__DIR__);
/*
|--------------------------------------------------------------------------
| 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);
});
+44 -2
View File
@@ -1,10 +1,52 @@
<?php <?php
declare(strict_types=1);
namespace Tests; 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; 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();
});
}
} }
+6 -6
View File
@@ -10,11 +10,11 @@
$events->shouldReceive('dispatch'); $events->shouldReceive('dispatch');
$this->cart = new Cart( $this->cart = new Cart(
new SessionMock(), new SessionMock,
$events, $events,
'cart', 'cart',
'SAMPLESESSIONKEY', 'SAMPLESESSIONKEY',
require(__DIR__ . '/../Helpers/ConfigConditionsMock.php') require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
); );
}); });
@@ -1065,7 +1065,7 @@
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90'); expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
$this->cart->clear(); $this->cart->clearItems();
$this->cart->add([ $this->cart->add([
'id' => 1, 'id' => 1,
@@ -1099,7 +1099,7 @@
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90'); expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
$this->cart->clear(); $this->cart->clearItems();
$this->cart->add([ $this->cart->add([
'id' => 1, 'id' => 1,
@@ -1201,7 +1201,7 @@
expect($this->cart->getConditions(active: true))->toHaveCount(0, 'Cart should have 0 conditions'); expect($this->cart->getConditions(active: true))->toHaveCount(0, 'Cart should have 0 conditions');
$this->cart->clear(); $this->cart->clearItems();
$this->cart->add([ $this->cart->add([
'id' => 1, 'id' => 1,
@@ -1310,7 +1310,7 @@
// now we should have only 1 condition left on that item // now we should have only 1 condition left on that item
expect($this->cart->get(1)['conditions'])->toHaveCount(1, 'Item should have one condition left'); expect($this->cart->get(1)['conditions'])->toHaveCount(1, 'Item should have one condition left');
expect($this->cart->get(1)['conditions'][0]->getName())->toEqual('Mother\'s Day Gift Pack'); expect($this->cart->get(1)['conditions'][1]->getName())->toEqual('Mother\'s Day Gift Pack');
}); });
test('can add item with multiple negative item conditions', function () { test('can add item with multiple negative item conditions', function () {
+12
View File
@@ -689,3 +689,15 @@
expect($this->cart->getTotalQuantity())->toBeInt('Return type should be INT'); expect($this->cart->getTotalQuantity())->toBeInt('Return type should be INT');
expect($this->cart->getTotalQuantity())->toEqual(12, 'Cart\'s quantity should be 4.'); expect($this->cart->getTotalQuantity())->toEqual(12, 'Cart\'s quantity should be 4.');
}); });
test('can add to cart and then update the session key', function () {
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
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');
$this->cart->setSessionKey('new_session_key');
expect($this->cart->getSessionKey())->toEqual('new_session_key', 'Session key should be updated to new_session_key');
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should still be 1');
});
+26 -26
View File
@@ -9,9 +9,9 @@
Mockery::close(); Mockery::close();
}); });
test('event cart created', function () { test('event cart Created', function () {
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$cart = new Cart( $cart = new Cart(
new SessionMock, new SessionMock,
@@ -25,10 +25,10 @@
}); });
test('event cart adding', function () { test('event cart adding', function () {
$events = Mockery::mock('Illuminate\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->once()->with('cart.adding', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Adding', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->once()->with('cart.added', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Added', Mockery::type('array'), true);
$cart = new Cart( $cart = new Cart(
new SessionMock, new SessionMock,
@@ -44,10 +44,10 @@
}); });
test('event cart adding multiple times', function () { test('event cart adding multiple times', function () {
$events = Mockery::mock('Illuminate\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(2)->with('cart.adding', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Adding', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(2)->with('cart.added', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Added', Mockery::type('array'), true);
$cart = new Cart( $cart = new Cart(
new SessionMock, new SessionMock,
@@ -64,10 +64,10 @@
}); });
test('event cart adding multiple times scenario two', function () { test('event cart adding multiple times scenario two', function () {
$events = Mockery::mock('Illuminate\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
$items = [ $items = [
[ [
@@ -107,12 +107,12 @@
}); });
test('event cart remove item', function () { test('event cart remove item', function () {
$events = Mockery::mock('Illuminate\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(1)->with('cart.removing', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removing', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(1)->with('cart.removed', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removed', Mockery::type('array'), true);
$items = [ $items = [
[ [
@@ -154,12 +154,12 @@
}); });
test('event cart clear', function () { test('event cart clear', function () {
$events = Mockery::mock('Illuminate\Events\Dispatcher'); $events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true); $events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->once()->with('cart.clearing', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Clearing', Mockery::type('array'), true);
$events->shouldReceive('dispatch')->once()->with('cart.cleared', Mockery::type('array'), true); $events->shouldReceive('dispatch')->once()->with('LaravelCart.Cleared', Mockery::type('array'), true);
$items = [ $items = [
[ [
+145
View File
@@ -0,0 +1,145 @@
<?php
use Wearepixel\Cart\Cart;
use Wearepixel\Cart\CartCondition;
use Wearepixel\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');
});
test('can add an item with an item level condition', function () {
$saleCondition = new CartCondition([
'name' => '5% Discount',
'type' => 'sale',
'value' => '-5%',
]);
$item = [
'id' => 1,
'name' => 'Chicken Curry',
'price' => 18.95,
'quantity' => 1,
'attributes' => [],
'conditions' => $saleCondition,
];
$this->cart->add($item);
expect($this->cart->get(1)->getPriceSum())->toEqual(18.95);
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(18);
expect($this->cart->getSubTotal())->toEqual(18, 'Cart should have subtotal of 18');
expect($this->cart->getTotal())->toEqual(18, 'Cart should have total of 18');
});
test('can add an item with multiple item level conditions', function () {
$saleCondition = new CartCondition([
'name' => '5% Discount',
'type' => 'sale',
'value' => '-5%',
]);
$gstCondition = new CartCondition([
'name' => 'GST',
'type' => 'gst',
'value' => '2.5%',
]);
$item = [
'id' => 1,
'name' => 'Chicken Curry',
'price' => 18.95,
'quantity' => 1,
'attributes' => [],
'conditions' => [$saleCondition, $gstCondition],
];
$this->cart->add($item);
expect($this->cart->get(1)->getPriceSum())->toEqual(18.95);
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(18.45);
expect($this->cart->getSubTotal())->toEqual(18.45, 'Cart should have subtotal of 18.45');
expect($this->cart->getTotal())->toEqual(18.45, 'Cart should have total of 18.45');
});
test('can clear a simple cart', 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->clear();
expect($this->cart->isEmpty())->toBeTrue('Cart should be empty');
expect($this->cart->getContent()->count())->toEqual(0, 'Cart content should be 0');
expect(MockCartModel::all()->count())->toEqual(0, 'The carts database table should have 0 rows');
});
test('can clear cart conditions after clearing the cart', function () {
$condition = new CartCondition([
'name' => '10% Off',
'type' => 'coupon',
'target' => 'subtotal',
'value' => '-10%',
]);
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
$this->cart->condition($condition);
$this->cart->clear();
// This was throwing "Call to a member function save() on array"
// because clear() replaced $this->session with a plain array
expect(fn () => $this->cart->clearCartConditions())->not->toThrow(Error::class);
expect($this->cart->getConditions())->toBeEmpty();
});
});
+14
View File
@@ -0,0 +1,14 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
// Suppress PHP 8.4 deprecation notices that originate from vendor code.
// These are typically from third-party packages (e.g. Mockery) that have not
// yet updated implicit-nullable parameter signatures and are not actionable here.
set_error_handler(static function (int $errno, string $errstr, string $errfile): bool {
if ($errno === E_DEPRECATED && str_contains($errfile, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) {
return true;
}
return false;
}, E_DEPRECATED);