DI and actions
The facade is the recommended default, not the only way in. There are three equivalent entry points, and all of them run the same code:
- The Coupons facade — the shortest form, recommended for everyday calls.
- The manager, RoundlyConsulting\Coupons\CouponManager — the facade root. Inject it through the constructor for the same API as an explicit dependency, with no static calls.
- Actions — single-purpose classes in RoundlyConsulting\Coupons\Actions with one execute() method, for composing into your own actions, jobs and commands.
Injecting the manager
use RoundlyConsulting\Coupons\CouponManager;
use RoundlyConsulting\Coupons\DataTransferObjects\RedemptionResult;
use RoundlyConsulting\Money\Money;
final class ApplyCoupon
{
public function __construct(private CouponManager $coupons) {}
public function __invoke(string $code, Money $cart, User $user): RedemptionResult
{
return $this->coupons->code($code)->redeem($cart, $user);
}
}Coupons::fake() swaps the manager for the facade and for every injected CouponManager — the manager isn’t final so the fake can subtype it — so a class that injects it is tested exactly like one that calls the facade.
Running an action
Each operation is also a plain action class you can resolve and run yourself. Most take plain arguments; CreateCouponAction takes a CreateCouponData DTO and RedeemCouponAction a RedeemCouponData DTO:
use RoundlyConsulting\Coupons\Actions\{CheckCouponAction, CreateCouponAction, ExpireCouponsAction,
PruneCouponsAction, RedeemCouponAction, RevokeCouponAction};
use RoundlyConsulting\Coupons\DataTransferObjects\RedeemCouponData;
app(CreateCouponAction::class)->execute($data); // Coupon; quiet: true skips CouponCreated
app(CheckCouponAction::class)->execute('SUMMER', $cart, $user); // ?RedemptionFailureReason
app(RedeemCouponAction::class)->execute(new RedeemCouponData(coupon: 'SUMMER', price: $cart, redeemer: $user));
app(RevokeCouponAction::class)->execute($coupon);
app(ExpireCouponsAction::class)->execute(); // int
app(PruneCouponsAction::class)->execute(days: 30, force: false); // intThe manager resolves each action from the container when a verb is called. An action you run yourself bypasses the manager, so Coupons::fake() doesn’t record it — in tests, assert on the database and events instead. The Coupon model, the HasCoupons trait, the Redeemable rule and both Artisan commands all go through the manager.
Facade method → action
| Facade method | Action | Notes |
|---|---|---|
generate(), create() | CreateCouponAction | Takes a CreateCouponData DTO; generate() builds it for you. |
createQuietly() | CreateCouponAction | execute($data, quiet: true) — no CouponCreated. |
check() | CheckCouponAction | execute($coupon, $price, $redeemer) — never throws, locks or writes. |
redeem() | RedeemCouponAction | Takes a RedeemCouponData DTO (coupon, price, redeemer). |
revoke() | RevokeCouponAction | Takes a Coupon model; the manager resolves a code first. |
expireAll() | ExpireCouponsAction | execute(?string $code = null): int |
prune() | PruneCouponsAction | execute(int $days = 30, bool $force = false): int |
preview() | — | Coupon::previewDiscount() on the resolved model. |
find(), findOrFail(), exists(), redeemable() | — | A query on the model configured in coupons.model. |
code() | — | A CouponCode handle over check(), preview(), redeem() and revoke(). |
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.