Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Vouchers Laravel Package

frittenkeez/laravel-vouchers

Laravel Vouchers offers an Eloquent-backed way to create, issue, and redeem voucher codes. Track usage, associate vouchers with models, and manage redemption state through a simple API for discounts, invites, credits, or other redeemable rewards.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its assets:

composer require frittenkeez/laravel-vouchers
php artisan vendor:publish --tag=vouchers-config
php artisan vendor:publish --tag=vouchers-migrations
php artisan migrate

Requirements for 0.9.x: Laravel 12.4+ (the #[Scope] attribute is required). Laravel 11 and PEST 3 are no longer supported — check composer.json before upgrading an existing app.

Where to look first:

  1. config/vouchers.php — code mask, prefix/suffix, counter options, model bindings. The new Config::getCode() and Config::hasCounterOptions() helpers are the canonical way to read this at runtime.
  2. The Vouchers facade docblock — now includes unredeem() and unredeemable() alongside the redeem methods.
  3. UPGRADING.md — the 0.9.0 guide is essential reading because of the scope rename, metadata cast change, and named-argument rename.

The quickest first use case: generate a small batch and redeem one.

use FrittenKeeZ\Vouchers\Facades\Vouchers;

$vouchers = Vouchers::withMask('****-****')->batch(10);

$voucher = Vouchers::redeem($vouchers->first()->code);
// or, if you already have the model loaded:
$voucher = Vouchers::redeem($vouchers->first());

redeem() returns the Voucher on success and null when the code is unknown or already redeemed, so a simple if (! $voucher) is usually all the failure handling you need for a first pass.

Implementation Patterns

Batch generation with a static code plus counter. The headline feature of 0.9.0. Previously, withCode() with an amount greater than one threw InfiniteLoopException. Now it appends a counter, which makes bulk issuance of human-readable sequential codes practical:

Vouchers::withCode('PROMO')
    ->withCounter()                  // enable counter
    ->withCounterStep(1)             // increment per voucher
    ->withCounterSeparator('-')      // PROMO-1, PROMO-2, ...
    ->withCounterPadding(6)          // PROMO-000001
    ->batch(100);

Use withoutCounterPadding() when you want bare counters (PROMO-1). withCounterStep() is useful for pre-allocating blocks or avoiding perceived gaps.

Custom code formatting. withCodeFormatter() receives a CodeFormat value object; casting it to string produces the package's default formatting. This is the extension point for wrapping codes in a checksum, a brand prefix, or a different separator convention without reimplementing the generator:

Vouchers::withCode('INV')
    ->withCounter()
    ->withCodeFormatter(fn (CodeFormat $code) => 'ACME-' . (string) $code)
    ->batch(50);

Batch creation cost. batch() and counter generation now validate candidate codes against the database in chunks of 500 rather than one query per code. Creating 100 vouchers went from 100 lookups to 1. Keep this in mind when sizing batches — very large batch() calls are now cheap on queries but still hold the full collection in memory, so chunk at the application level for tens of thousands.

Passing Voucher instances to redeem methods. redeem(), unredeem(), redeemable() and unredeemable() accept a model or a code. In a controller that already resolved a Voucher via route model binding, pass the instance to skip a redundant lookup:

Route::post('/vouchers/{voucher}/redeem', function (Voucher $voucher) {
    return Vouchers::redeem($voucher) ? 'ok' : 'already used';
});

Metadata as a mutable ArrayObject. Voucher::$metadata and Redeemer::$metadata are now cast to an array object, so you can mutate in place and save() without reassigning the whole attribute:

$voucher->metadata['channel'] = 'sms';
$voucher->metadata['sent_at'] = now();
$voucher->save();

For nullable metadata columns, use the new FrittenKeeZ\Vouchers\Casts\AsNullableArrayObject. It behaves like Laravel's AsArrayObject but preserves null rather than writing the JSON string 'null' — important if you have queries or reporting that distinguish "no metadata" from "empty metadata".

Owners and entities. Voucher::withOwner() and VoucherEntity::withEntity() now use the given instance's own getMorphClass(). If your User or Order model overrides getMorphClass() without a morph map entry, relation queries now match the rows that were written. This mainly matters in multi-tenant or polymorphic setups where morph aliases are customised.

Scoping. All scopes now use the #[Scope] attribute and are protected. Day-to-day usage is unchanged in the query builder:

Voucher::withCode($code)->first();
Voucher::withMask($mask)->get();

Gotchas and Tips

Voucher::code() was renamed to Voucher::withCode(). A scope named code shadows the code column, which breaks $voucher->code when the attribute isn't loaded. Search for Voucher::code( and ->code( scope calls when upgrading — the latter is hard to grep because it's also legitimate attribute access.

Named arguments changed. The first parameter of redeem(), unredeem(), redeemable() and unredeemable() is now $voucher, not $code. Positional calls are unaffected; Vouchers::redeem(code: $code) will now fatal.

metadata is no longer a plain array. Passing it to array_* functions, json_encode assumptions, or typed properties will break. Call ->toArray() first. If you have array type hints or casts layered on top of the package's cast, remove them — the package owns the cast now.

Static masks still throw, counters don't. withCode() with an amount greater than one now appends a counter. withMask() with a static mask (no asterisks) still throws InfiniteLoopException. If you were relying on the old exception as a guard, that behaviour is gone for withCode().

Counter options plus asterisks is an error. Combining counter options with a mask containing * throws the new CounterException. Check Config::hasCounterOptions() before conditionally applying a mask — this is the most likely place to hit the exception when config and call-site options are mixed.

Scope methods are protected. Only relevant if you extend Voucher or Redeemer and override a scope. Overrides must match the new visibility, and any package-internal call to $this->scopeXxx() style helpers is gone.

Migrations only affect new installs. The published migration now uses json for metadata and modern $table->id() / foreignId()->constrained()->cascadeOnDelete() syntax. Existing installs keep text columns and continue to work — do not hand-edit production migrations to match, and do not assume a fresh migration diff means a schema change is required.

Debugging code generation. Log Config::getCode() and the resolved counter options when codes come out unexpectedly. Most "wrong code" reports trace back to config defaults being merged with call-site fluent options, or to a formatter that was applied but whose output was not what the caller expected.

Extension points to prefer. withCodeFormatter() plus CodeFormat for output shape, the AsNullableArrayObject cast for nullable JSON columns, and morph-class-aware owner/entity relations. Avoid subclassing the models to alter code generation — the formatter is the supported seam and survives the scope-visibility change.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox