Деяким Laravel-застосункам недостатньо простої знижки у відсотках. Для сезонних розпродажів або запуску нових продуктів потрібні гнучкі інструменти: обмеження максимальної суми, мінімальний чек, ліміти використання та терміни дії. Пакет Laravel Discount від Milwad Khosravi зберігає ці параметри в Eloquent-моделях та дозволяє керувати ними через єдиний фасад.
Ось основні можливості пакета:
- Два типи знижок:
DiscountType::Percentage(відсоткова) таDiscountType::Fixed(фіксована) з можливістю встановити лімітmax_discount_amount. - Промокоди та автоматичні знижки: наявність
codeперетворює знижку на купон, інакше вона застосовується автоматично. - Часові межі: колонки
starts_atтаexpires_atразом зі scope-запитомvalid()для перевірки актуальності. - Ліміти використання: загальний
usage_limitта персональнийusage_limit_per_userдля кожного клієнта. - Підтримка гостей: використання session ID для контролю лімітів неавторизованих користувачів.
- Правила сумування: якщо знижка позначена як
is_stackable, пакет сам обчислить найвигіднішу комбінацію для клієнта. - Знижки для моделей: трейт
HasDiscountsдозволяє прив'язати знижки до будь-якої Eloquent-моделі. - Інтеграція з кошиком: сервіс
CartDiscountдля застосування кодів до всього кошика або окремих позицій.
# Відсоткові та фіксовані знижки
Знижка — це стандартна Eloquent-модель, тому створити її можна як звичайний запис:
use Binafy\LaravelDiscount\Enums\DiscountType;
use Binafy\LaravelDiscount\Models\Discount;
$discount = Discount::query()->create([
'name' => 'Summer Sale',
'type' => DiscountType::Percentage,
'value' => 20,
]);
Застосування відбувається через фасад LaravelDiscount, який валідує знижку, проводить обчислення та повертає об'єкт DiscountResult:
use Binafy\LaravelDiscount\Facades\LaravelDiscount;
$result = LaravelDiscount::apply($discount, 200);
$result->originalAmount; // 200.0
$result->discountAmount; // 40.0
$result->payableAmount(); // 160.0
Об'єкт DiscountResult містить інформацію про застосовані знижки, початкову суму та розраховану вигоду. Метод payableAmount() віднімає знижку від загальної суми, гарантуючи, що результат не буде меншим за нуль.
Будь-який тип знижки може мати «стелю» завдяки max_discount_amount. Це ідеально підходить для акцій на кшталт «-20%, але не більше 100 грн», що позбавляє необхідності прописувати таку логіку в контролерах:
$discount = Discount::query()->create([
'code' => 'SAVE20',
'type' => DiscountType::Percentage,
'value' => 20,
'max_discount_amount' => 100,
]);
LaravelDiscount::apply($discount, 300)->discountAmount; // 60.0
LaravelDiscount::apply($discount, 1000)->discountAmount; // 100.0
# Промокоди, терміни дії та ліміти
Додавання значення в колонку code робить знижку промокодом. Метод applyCode() перевіряє код і викидає DiscountNotFoundException, якщо його не знайдено:
$result = LaravelDiscount::applyCode('WELCOME10', 200, $user);
Якщо для кампанії потрібні унікальні коди для кожного клієнта, пакет генерує їх за допомогою random_int(), виключаючи символи, які легко переплутати (наприклад, 0/O або 1/I):
LaravelDiscount::generateCode(); // "8FJ2K9QW"
LaravelDiscount::generateCodes(100, 'VIP'); // Колекція зі 100 унікальних кодів
Довжину, алфавіт, префікс та роздільник можна налаштувати у файлі config/laravel-discount.php.
Для обмежених у часі пропозицій використовуються starts_at та expires_at. Якщо спробувати застосувати знижку завчасно або після дедлайну, пакет викине відповідний виняток (DiscountNotStartedException або DiscountExpiredException) та запустить подію DiscountExpired. Отримати список актуальних знижок можна через scope-запит:
Discount::query()->valid()->get();
Загальний ліміт використання (usage_limit) та ліміт на користувача (usage_limit_per_user) остаточно фіксуються методом redeem() під час завершення замовлення:
LaravelDiscount::redeem($discount, $user, $result->discountAmount);
Цей процес виконується в межах транзакції. База даних сама вирішує, чи можна інкрементувати used_count. Якщо ліміт вичерпано, виникне DiscountUsageLimitReachedException. Це гарантує, що два клієнти не зможуть одночасно використати останній доступний купон.
Для гостей замість моделі користувача використовується session ID, що відстежується в колонці session_id таблиці discount_usages:
$result = LaravelDiscount::applyCode('GUEST10', $total, sessionId: session()->getId());
LaravelDiscount::redeem($discount, amount: $result->discountAmount, sessionId: session()->getId());
# Умови та сумування знижок
Колонка min_order_value дозволяє встановити поріг суми замовлення, нижче якого знижка не діятиме. Також є JSON-колонка conditions для зберігання кастомних умов. Якщо ви віддаєте перевагу складним умовам у вигляді PHP-об'єктів, варто звернути увагу на пакет Discountify.
Знижки можна прив’язувати безпосередньо до моделей через трейт HasDiscounts та поліморфний зв'язок у таблиці discountables:
use Binafy\LaravelDiscount\Traits\HasDiscounts;
class Product extends Model
{
use HasDiscounts;
}
$product->discounts()->attach($discount);
$product->validDiscounts();
$product->hasDiscount('TECH10');
$result = $product->applyDiscounts($product->price);
Метод applyDiscounts використовує логіку applyMany() для сумування. Пакет відсіює невалідні знижки, додає всі доступні до сумування (stackable), порівнює результат із найкращою некумулятивною знижкою та повертає найбільш вигідний для клієнта варіант.
# Інтеграція з Laravel Cart
Якщо ви використовуєте binafy/laravel-cart від того ж автора, вам стає доступним сервіс CartDiscount для роботи зі знижками на рівні кошика або окремих товарів:
use Binafy\LaravelDiscount\Integrations\LaravelCart\CartDiscount;
$cartDiscount = app(CartDiscount::class);
$result = $cartDiscount->applyToCart($cart, 'SUMMER-8FJ2K9QW');
$result = $cartDiscount->applyToItem($cartItem, $discount);
$result = $cartDiscount->applyItemDiscounts($cart);
Метод applyToCart() автоматично перевіряє мінімальну суму замовлення та ліміти користувача. applyToItem() розраховує знижку для конкретного рядка кошика, а applyItemDiscounts() проходить по всьому кошику та застосовує знижки, прив'язані до моделей товарів через трейт HasDiscounts.
# Валідація, винятки та події
Для перевірки промокодів у формах передбачено правило ValidDiscountCode. Воно повертає конкретну причину відмови, а не просто загальне повідомлення про помилку:
use Binafy\LaravelDiscount\Rules\ValidDiscountCode;
public function rules(): array
{
return [
'code' => ['required', new ValidDiscountCode(
orderAmount: $this->cartTotal(),
user: $this->user(),
)],
];
}
Кожен випадок невдачі має свій виняток (Exception), що наслідується від DiscountException. Ви можете перехоплювати конкретні помилки (наприклад, DiscountExpiredException), щоб виводити кастомні повідомлення для користувачів.
Життєвий цикл знижки супроводжується трьома подіями: DiscountApplied (застосовано до суми), DiscountRedeemed (використано в замовленні) та DiscountExpired (спроба використати прострочену знижку). Подія DiscountRedeemed передає дані про знижку та запис про її використання, що зручно для аналітики або сповіщень.
# Встановлення
Laravel Discount потребує PHP 8.1+ та Laravel 9-13. Встановіть пакет через Composer та запустіть міграції:
composer require binafy/laravel-discount
php artisan migrate
Пакет також містить дві Artisan-команди: discount:generate для створення кодів через термінал та discount:prune для видалення застарілих знижок, яку варто додати до планувальника завдань:
Schedule::command('discount:prune --days=30')->daily();
Повну документацію та опис конфігурації можна знайти в репозиторії Laravel Discount на GitHub.