laravel-cart maintained by timurturdyev
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.