daikazu/flexicart

A flexible shopping cart package for Laravel

Fund package maintenance!
Daikazu

v1.0.0-beta 2025-06-25 13:47 UTC

This package is auto-updated.

Last update: 2025-06-27 17:36:32 UTC


README

Logo for Flexi Cart

FlexiCart

PHP Version Require Laravel Version Latest Version on Packagist GitHub Tests Action Status PHPStan Total Downloads GitHub forks GitHub stars

A flexible shopping cart package for Laravel with support for session or database storage, conditional pricing, and custom product attributes.

Table of Contents

Features

  • Flexible Storage: Use session storage (default) or database storage
  • Cart Item Conditions: Apply discounts, fees, or any adjustments to items
    • Percentage-based adjustments (e.g., 10% discount)
    • Fixed-amount adjustments (e.g., $5 off, $2 add-on fee)
    • Stack multiple conditions on the same item
  • Custom Product Attributes: Store any item-specific attributes (color, size, etc.)
  • Global Cart Conditions: Apply conditions to the cart subtotal or only to taxable items
  • Precise Price Handling: Uses Brick/Money for accurate currency calculations
  • Taxable Item Support: Mark specific items as taxable or non-taxable
  • Easy Integration: Simple API with Laravel Facade
  • Comprehensive Tests: Unit and feature tests included

Installation

Requirements

  • PHP 8.3 or higher
  • Laravel 11.0 or higher
  • Brick/Money package (automatically installed)

Install the Package

You can install the package via composer:

composer require daikazu/flexicart

Publish Configuration

Publish the configuration file to customize the package settings:

php artisan vendor:publish --tag="flexicart-config"

This will publish the configuration file to config/flexicart.php.

Database Storage (Optional)

If you want to use database storage instead of session storage, you need to run the migrations:

php artisan vendor:publish --tag="flexicart-migrations"
php artisan migrate

Then update your .env file:

CART_STORAGE=database

Basic Usage

Adding Items to the Cart

You can add items to the cart using the addItem method. Items can be added as arrays or CartItem objects:

use Daikazu\Flexicart\Facades\Cart;

// Add item as array
Cart::addItem([
    'id' => 1,
    'name' => 'Product Name',
    'price' => 29.99,
    'quantity' => 2,
    'attributes' => [
        'color' => 'red',
        'size' => 'large'
    ]
]);

// Add multiple items at once
Cart::addItem([
    [
        'id' => 2,
        'name' => 'Another Product',
        'price' => 15.50,
        'quantity' => 1
    ],
    [
        'id' => 3,
        'name' => 'Third Product',
        'price' => 45.00,
        'quantity' => 3
    ]
]);

Updating Items in the Cart

Update existing items using the updateItem method:

// Update quantity
Cart::updateItem('item_id', ['quantity' => 5]);

// Update attributes
Cart::updateItem('item_id', [
    'attributes' => [
        'color' => 'blue',
        'size' => 'medium'
    ]
]);

// Update multiple properties
Cart::updateItem('item_id', [
    'quantity' => 3,
    'price' => 25.99,
    'attributes' => ['color' => 'green']
]);

Removing Items from the Cart

Remove items individually or clear the entire cart:

// Remove a specific item
Cart::removeItem('item_id');

// Clear all items from the cart
Cart::clear();

Getting Cart Content and Calculations

Access cart items and perform calculations:

// Get all items
$items = Cart::items();

// Get a specific item
$item = Cart::item('item_id');

// Get cart counts
$totalItems = Cart::count(); // Total quantity of all items
$uniqueItems = Cart::uniqueCount(); // Number of unique items

// Check if cart is empty
$isEmpty = Cart::isEmpty();

// Get cart totals
$subtotal = Cart::subtotal(); // Subtotal before conditions
$total = Cart::total(); // Final total after all conditions
$taxableSubtotal = Cart::getTaxableSubtotal(); // Subtotal of taxable items only

Understanding Conditions

Conditions are adjustments that can be applied to cart items or the entire cart. There are several types:

  • Percentage Conditions: Apply percentage-based adjustments (e.g., 10% discount)
  • Fixed Conditions: Apply fixed-amount adjustments (e.g., $5 off)
  • Tax Conditions: Special conditions for tax calculations

Conditions can target:

  • Individual Items: Applied to specific cart items
  • Cart Subtotal: Applied to the entire cart subtotal
  • Taxable Items: Applied only to items marked as taxable

Adding Conditions

Add conditions to the cart or specific items:

use Daikazu\Flexicart\Conditions\Types\PercentageCondition;
use Daikazu\Flexicart\Conditions\Types\FixedCondition;
use Daikazu\Flexicart\Enums\ConditionTarget;

// Add a 10% discount to the cart using condition class
$discount = new PercentageCondition(
    name: '10% Off Sale',
    value: -10, // Negative for discount
    target: ConditionTarget::SUBTOTAL
);
Cart::addCondition($discount);

// Add a $5 shipping fee using condition class
$shipping = new FixedCondition(
    name: 'Shipping Fee',
    value: 5.00,
    target: ConditionTarget::SUBTOTAL
);
Cart::addCondition($shipping);

// Add condition using array format with specific condition class
$memberDiscount = PercentageCondition::make([
    'name' => 'Member Discount',
    'value' => -15,
    'target' => ConditionTarget::SUBTOTAL
]);
Cart::addCondition($memberDiscount);

// Add condition to a specific item
$itemDiscount = new PercentageCondition(
    name: 'Item Discount',
    value: -20,
    target: ConditionTarget::ITEM
);
Cart::addItemCondition('item_id', $itemDiscount);

// Add multiple conditions at once
Cart::addConditions([
    new FixedCondition('Processing Fee', 2.50, ConditionTarget::SUBTOTAL),
    new PercentageCondition('Bulk Discount', -5, ConditionTarget::SUBTOTAL)
]);

Removing Conditions

Remove conditions from the cart or items:

// Remove a specific condition from the cart
Cart::removeCondition('10% Off Sale');

// Remove a condition from a specific item
Cart::removeItemCondition('item_id', 'Item Discount');

// Clear all cart conditions
Cart::clearConditions();

Marking Items as Non-Taxable

By default, all items are taxable. You can mark specific items as non-taxable:

// Add non-taxable item
Cart::addItem([
    'id' => 4,
    'name' => 'Non-taxable Service',
    'price' => 100.00,
    'quantity' => 1,
    'taxable' => false
]);

// Update existing item to be non-taxable
Cart::updateItem('item_id', ['taxable' => false]);

Configuration Options

Storage Configuration

Configure how cart data is stored:

// config/flexicart.php

// Use session storage (default)
'storage' => 'session',

// Use database storage
'storage' => 'database',

// Use custom storage class
'storage_class' => App\Services\CustomCartStorage::class,

Session Key Configuration

Customize the session key when using session storage:

'session_key' => 'my_custom_cart_key',

Currency and Locale Configuration

Set the default currency and locale for price formatting:

'currency' => 'USD', // ISO currency code
'locale' => 'en_US', // Locale for formatting

Custom Models Configuration

When using database storage, you can specify custom models:

'cart_model' => App\Models\CustomCart::class,
'cart_item_model' => App\Models\CustomCartItem::class,

Compound Discounts

Control how multiple discounts are calculated:

// Sequential calculation (each discount applies to the result of previous discounts)
'compound_discounts' => true,

// Parallel calculation (all discounts apply to the original price)
'compound_discounts' => false,

Cart Cleanup

Configure automatic cleanup of old carts when using database storage:

'cleanup' => [
    'enabled' => true,
    'lifetime' => 60 * 24 * 7, // 1 week in minutes
],

Working with Prices

FlexiCart uses the Brick/Money library for precise price calculations.

Creating Price Objects

use Daikazu\Flexicart\Price;

// Create from numeric value
$price = Price::from(29.99);

// Create with specific currency
$price = Price::from(29.99, 'EUR');

// Create from Money object
$money = \Brick\Money\Money::of(29.99, 'USD');
$price = new Price($money);

Price Operations

$price1 = Price::from(10.00);
$price2 = Price::from(5.00);

// Addition
$total = $price1->plus($price2); // $15.00

// Subtraction
$difference = $price1->subtract($price2); // $5.00

// Multiplication
$doubled = $price1->multiplyBy(2); // $20.00

// Division
$half = $price1->divideBy(2); // $5.00

Formatting and Conversion

$price = Price::from(1234.56);

// Get formatted string
$formatted = $price->formatted(); // "$1,234.56"

// Get raw numeric value
$amount = $price->toFloat(); // 1234.56

// Get string representation
$string = (string) $price; // "$1,234.56"

// Get minor value (cents)
$cents = $price->getMinorValue(); // 123456

Displaying the Cart in Blade

Here's an example of how to display cart contents in a Blade template:

{{-- resources/views/cart.blade.php --}}
@if(Cart::isEmpty())
    <p>Your cart is empty.</p>
@else
    <div class="cart">
        <h2>Shopping Cart</h2>

        <div class="cart-items">
            @foreach(Cart::items() as $item)
                <div class="cart-item">
                    <h4>{{ $item->name }}</h4>
                    <p>Price: {{ $item->unitPrice()->formatted() }}</p>
                    <p>Quantity: {{ $item->quantity }}</p>
                    <p>Subtotal: {{ $item->subtotal()->formatted() }}</p>

                    @if($item->attributes)
                        <div class="attributes">
                            @foreach($item->attributes as $key => $value)
                                <span class="attribute">{{ $key }}: {{ $value }}</span>
                            @endforeach
                        </div>
                    @endif

                    <form action="{{ route('cart.remove') }}" method="POST">
                        @csrf
                        <input type="hidden" name="item_id" value="{{ $item->id }}">
                        <button type="submit">Remove</button>
                    </form>
                </div>
            @endforeach
        </div>

        <div class="cart-totals">
            <p>Subtotal: {{ Cart::subtotal()->formatted() }}</p>
            <p><strong>Total: {{ Cart::total()->formatted() }}</strong></p>
        </div>

        <div class="cart-actions">
            <a href="{{ route('cart.clear') }}" class="btn btn-secondary">Clear Cart</a>
            <a href="{{ route('checkout') }}" class="btn btn-primary">Checkout</a>
        </div>
    </div>
@endif

Database Storage Notes

When using database storage, FlexiCart creates the following tables:

  • carts: Stores cart metadata (ID, user association, timestamps)
  • cart_items: Stores individual cart items and their properties

Cart Persistence

With database storage, carts are automatically persisted. You can also manually persist session-based carts:

// Manually persist cart data
Cart::persist();

// Get raw cart data for custom storage
$cartData = Cart::getRawCartData();

Multiple Carts

You can work with multiple carts by specifying cart IDs:

// Get a specific cart
$cart = Cart::getCartById('user_123_cart');

// Switch to a different cart
$guestCart = Cart::getCartById('guest_cart');

Extending the Package

Custom Condition Class

Create custom condition types by extending the base Condition class:

<?php

namespace App\Cart\Conditions;

use Daikazu\Flexicart\Conditions\Condition;
use Daikazu\Flexicart\Enums\ConditionType;
use Daikazu\Flexicart\Price;

class BuyOneGetOneCondition extends Condition
{
    public ConditionType $type = ConditionType::PERCENTAGE;

    public function calculate(?Price $price = null): Price
    {
        // Custom calculation logic
        // For example, buy one get one 50% off
        if ($this->attributes->quantity >= 2) {
            $discount = $price->multipliedBy(0.5);
            return $discount->multipliedBy(-1); // Negative for discount
        }

        return Price::from(0);
    }

    public function formattedValue(): string
    {
        return 'Buy One Get One 50% Off';
    }
}

Custom Storage Method

Implement custom storage by creating a class that implements StorageInterface:

<?php

namespace App\Cart\Storage;

use Daikazu\Flexicart\Contracts\StorageInterface;

class RedisCartStorage implements StorageInterface
{
    public function get(string $key): array
    {
        // Implement Redis get logic
    }

    public function put(string $key, array $data): void
    {
        // Implement Redis put logic
    }

    public function forget(string $key): void
    {
        // Implement Redis forget logic
    }

    public function flush(): void
    {
        // Implement Redis flush logic
    }
}

Then configure it in your flexicart.php config file:

'storage_class' => App\Cart\Storage\RedisCartStorage::class,

Testing

The package comes with comprehensive tests. To run them:

composer test

Troubleshooting

Common Issues

Cart data not persisting between requests

  • Ensure sessions are properly configured in your Laravel application
  • If using database storage, verify migrations have been run
  • Check that the CART_STORAGE environment variable is set correctly

Price calculation errors

  • Verify that all price values are numeric
  • Ensure currency codes are valid ISO codes
  • Check that the Brick/Money library is properly installed

Condition not applying correctly

  • Verify condition targets are set appropriately
  • Check condition order values if multiple conditions exist
  • Ensure condition values are properly signed (negative for discounts)

Memory issues with large carts

  • Consider implementing cart item limits
  • Use database storage for better memory management
  • Implement cart cleanup for old/abandoned carts

Debug Mode

Enable debug mode to get more detailed error messages:

// In your AppServiceProvider or cart usage
config(['app.debug' => true]);

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.