timurturdyev/laravel-cart

Shopping cart, wishlist and compare lists for Laravel

Maintainers

Package info

github.com/TimurTurdyev/laravel-cart

pkg:composer/timurturdyev/laravel-cart

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-30 21:06 UTC

This package is auto-updated.

Last update: 2026-07-30 21:18:39 UTC


README

Laravel Cart

Laravel Cart

Корзина, закладки и сравнение товаров для Laravel 12+ (PHP 8.3+).

Каждый список (корзина, закладки, сравнение, свои списки) - один и тот же примитив с политикой из конфига. Деньги считает только корзина, и делает это через конвейер классов-корректировок. Без магических строк, без float, без обязательных миграций.

Установка

composer require timurturdyev/laravel-cart
php artisan vendor:publish --tag=cart-config   # по желанию

Провайдер подхватывается автоматически через package discovery.

Быстрый старт

use TimurTurdyev\Cart\Facades\Cart;
use TimurTurdyev\Cart\Facades\Compare;
use TimurTurdyev\Cart\Facades\Wishlist;

$line = Cart::add($item, quantity: 2, options: ['size' => 'm']);

Cart::setQuantity($line->id, 5);
Cart::total();                // Price: ->minor(), ->decimal(), ->format()

Wishlist::toggle($item);      // первый вызов добавляет, второй убирает
Wishlist::has($item);         // для кнопки-сердечка
Wishlist::moveToCart($item);

Compare::add($item);          // лимит берется из конфига

$item - любая модель, реализующая Purchasable:

use TimurTurdyev\Cart\Contracts\Purchasable;
use TimurTurdyev\Cart\Support\Price;

class Chair extends Model implements Purchasable
{
    public function cartId(): string|int
    {
        return $this->id;
    }

    public function cartName(): string
    {
        return $this->name;
    }

    public function cartPrice(): Price
    {
        return Price::fromMinor($this->price);
    }
}

Без модели строка собирается вручную:

use TimurTurdyev\Cart\Line;

Cart::add(Line::of(11, 'Chair', Price::fromDecimal('19.99'), quantity: 2));

Идентичность строки

Id строки = hash(id товара + нормализованные опции). Один товар с разными опциями сам раскладывается по разным строкам, составные id вручную склеивать не нужно:

Cart::add($item, options: ['size' => 'm']);
Cart::add($item, options: ['size' => 'l']);    // вторая строка

Cart::has($item, options: ['size' => 'm']);    // true

Данные вне идентичности (зафиксированная картинка, метка времени) живут в meta:

$line = Cart::add($item, meta: ['image' => $url]);

$line->meta('image');
$line->model();     // ленивый поиск модели по классу товара

Деньги

Суммы хранятся в целых минорных единицах внутри value-объекта Price. Конверсия в одной точке, округление half-up:

Price::fromMinor(1999);        // 19.99
Price::fromDecimal('19.99');   // строка парсится точно

Cart::total()->minor();        // 1999
Cart::total()->format();       // "19.99"

Скидки, сборы, доставка

use TimurTurdyev\Cart\Adjusters\PercentageDiscount;
use TimurTurdyev\Cart\Adjusters\Shipping;

Cart::adjust(
    new PercentageDiscount('summer', percent: 10),
    new Shipping(Price::fromMinor(1500)),
);

Cart::withoutAdjuster('summer');
Cart::totals()->breakdown();    // subtotal, каждая корректировка, total

В комплекте: PercentageDiscount, FixedDiscount, PercentageFee, Shipping. Корректировки применяются конвейером: каждая видит текущий total, поэтому последовательные скидки компаундятся и порядок важен. Своя корректировка - один класс:

use TimurTurdyev\Cart\Contracts\Adjuster;
use TimurTurdyev\Cart\Support\Totals;

final readonly class GiftWrap implements Adjuster
{
    public function name(): string
    {
        return 'gift-wrap';
    }

    public function adjust(Totals $totals): Totals
    {
        return $totals->addFee($this->name(), Price::fromMinor(300));
    }

    public function toArray(): array
    {
        return [];
    }

    public static function fromArray(array $data): static
    {
        return new self();
    }
}

Между запросами корректировки хранятся парами {class, data} и восстанавливаются с проверкой контракта. unserialize не используется.

Списки

'lists' => [
    'cart' => ['policy' => 'append'],
    'wishlist' => ['policy' => 'toggle'],
    'compare' => ['policy' => 'toggle', 'limit' => 4],
    'viewed' => ['policy' => 'toggle', 'limit' => 20],
],
политика поведение
append повторное добавление суммирует количество
toggle повторный add ничего не меняет, toggle() убирает
limit новая строка сверх лимита кидает ListLimitException

Новый тип списка - строка в конфиге, а не новый класс:

use TimurTurdyev\Cart\CartManager;

app(CartManager::class)->list('viewed')->toggle($item);

Cart::moveTo('wishlist', $line->id);   // отложить на потом
Wishlist::moveToCart($item);

Хранилище

По умолчанию session: работает сразу после установки, пустые списки не оставляют записей. Переключение на базу - одна строка конфига:

'storage' => 'database',
php artisan vendor:publish --tag=cart-migrations
php artisan migrate

При логине гостевая корзина сливается с корзиной пользователя. Стратегии: sum (количества складываются), keep (строки пользователя важнее), replace (гостевая заменяет).

Свой драйвер подключается снаружи, без правки пакета:

use TimurTurdyev\Cart\Storage\StorageManager;

app(StorageManager::class)->extend('redis', fn () => new RedisCartStorage());

События

LineAdded, LineUpdated, LineRemoved, ListCleared. В каждом - имя списка и строка. Отключаются через 'events' => false.

Переход с darryldecode/laravelshoppingcart

Таблица соответствия API - в UPGRADE.md. Сохраненные корзины конвертируются командой:

php artisan cart:import-legacy legacy_carts --owner-column=identifier --data-column=cart_data

Тесты

make check

Лицензия

MIT. См. LICENSE.md.