Install this package via Composer:
composer require frittenkeez/laravel-vouchersPlease read the upgrade guide.
Please read the release notes.
Publish config using Artisan command:
php artisan vendor:publish --tag=config --provider="FrittenKeeZ\Vouchers\VouchersServiceProvider"Publish migrations using Artisan command:
php artisan vendor:publish --tag=migrations --provider="FrittenKeeZ\Vouchers\VouchersServiceProvider"Don't forget to run migrations:
php artisan migrateChange basic configuration through config/vouchers.php - it should be well documented, so no need to describe all options here.
This package comes with an ease-of-use facade Vouchers with FQN FrittenKeeZ\Vouchers\Facades\Vouchers.
Generating codes without checking if they exist; defaults from config will be used if not specified.
Vouchers::generate(string|null $mask = null, string|null $characters = null): string;
$code = Vouchers::generate('***-***-***', '1234567890');Batch generation of codes is also possible; these will be checked against existing codes.
Vouchers::batch(int amount): array;
$codes = Vouchers::batch(10);Generating one or more vouchers is just as simple.
Vouchers::create(int $amount = 1): object|array;
$voucher = Vouchers::create();
$vouchers = Vouchers::create(10);Redeeming vouchers requires that you provide a redeemer entity.
Additional metadata for the redeemer can be provided.
Vouchers::redeem(string $code, Illuminate\Database\Eloquent\Model $entity, array $metadata = []): bool;
try {
$success = Vouchers::redeem('123-456-789', $user, ['foo' => 'bar']);
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherNotFoundException $e) {
// Voucher was not found with the provided code.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherRedeemedException $e) {
// Voucher has already been redeemed.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherUnstartedException $e) {
// Voucher is not yet started.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherExpiredException $e) {
// Voucher is expired.
}Or if you don't care about the specific exceptions:
try {
$success = Vouchers::redeem('123-456-789', $user, ['foo' => 'bar']);
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherException $e) {
// Voucher was not possible to redeem.
}Unredeeming voucher can be done by either providing a related redeemer entity, or by using a redeemer query filter to let the package find the redeemer for you.
Vouchers::unredeem(string $code, Illuminate\Database\Eloquent\Model|null $entity = null, Closure(Illuminate\Database\Eloquent\Builder)|null $callback = null): bool;
try {
$success = Vouchers::unredeem('123-456-789', $user);
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherNotFoundException $e) {
// Voucher was not found with the provided code.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherRedeemerNotFoundException $e) {
// Voucher redeemer was not found.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherUnstartedException $e) {
// Voucher is not yet started.
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherExpiredException $e) {
// Voucher is expired.
}Or if you don't care about the specific exceptions:
try {
$success = Vouchers::unredeem('123-456-789', $user);
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherException $e) {
// Voucher was not possible to unredeem.
}Without specifying the redeemer entity, which will use the first redeemer found:
try {
$success = Vouchers::unredeem('123-456-789');
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherException $e) {
// Voucher was not possible to unredeem.
}With specifying a redeemer query filter:
try {
$success = Vouchers::unredeem(code: '123-456-789', callback: fn (Illuminate\Database\Eloquent\Builder $query) => $query->where('metadata->foo', 'bar));
} catch (FrittenKeeZ\Vouchers\Exceptions\VoucherException $e) {
// Voucher was not possible to unredeem.
}Besides defaults specified in config/vouchers.php, you can override options when generating codes or creating vouchers.
Overriding model class names on runtime can be done using these methods.
// Override model class names.
Config::withModels(string|null $voucher = null, string|null $redeemer = null, string|null $entity = null);
// Reset model class names.
Config::resetModels();Following methods apply to Vouchers::generate(), Vouchers::batch() and Vouchers::create() calls.
// Override characters list.
Vouchers::withCharacters(string|null $characters);
// Override code mask.
Vouchers::withMask(string|null $mask);
// Override code prefix.
Vouchers::withPrefix(string|null $prefix);
// Disable code prefix.
Vouchers::withoutPrefix();
// Override code suffix.
Vouchers::withSuffix(string|null $suffix);
// Disable code suffix.
Vouchers::withoutSuffix();
// Override prefix and suffix separator.
Vouchers::withSeparator(string|null $separator);
// Disable prefix and suffix separator.
Vouchers::withoutSeparator();
// Override code mask and disable prefix, suffix and separator.
Vouchers::withCode(string $code);Following methods only apply to Vouchers::create() call.
// Add metadata to voucher.
Vouchers::withMetadata(array|null $metadata);
// Set voucher start time.
Vouchers::withStartTime(DateTime|null $timestamp);
// Set voucher start time using interval.
Vouchers::withStartTimeIn(DateInterval|null $interval);
// Set voucher start date - time component is zeroed.
Vouchers::withStartDate(DateTime|null $timestamp);
// Set voucher start date using interval - time component is zeroed.
Vouchers::withStartDateIn(DateInterval|null $interval);
// Set voucher expire time.
Vouchers::withExpireTime(DateTime|null $timestamp);
// Set voucher expire time using interval.
Vouchers::withExpireTimeIn(DateInterval|null $interval);
// Set voucher expire date - time component is set to end of day (23:59:59).
Vouchers::withExpireDate(DateTime|null $timestamp);
// Set voucher expire date using interval - time component is set to end of day (23:59:59).
Vouchers::withExpireDateIn(DateInterval|null $interval);
// Set related entities to voucher - using spread operater.
Vouchers::withEntities(Illuminate\Database\Eloquent\Model ...$entities);
// Set related entities to voucher - iterable.
Vouchers::withEntities(Illuminate\Database\Eloquent\Model[] $entities);
Vouchers::withEntities(Illuminate\Support\Collection<Illuminate\Database\Eloquent\Model> $entities);
Vouchers::withEntities(Generator<Illuminate\Database\Eloquent\Model> $entities);
// Set owning entity for voucher.
Vouchers::withOwner(Illuminate\Database\Eloquent\Model|null $owner);All calls are chainable and dynamic options will be reset when calling Vouchers::create() or Vouchers::reset().
$voucher = Vouchers::withMask('***-***-***')
->withMetadata(['foo' => 'bar'])
->withExpireDateIn(CarbonInterval::create('P30D'))
->create();
$voucher = Vouchers::withOwner($user)->withPrefix('USR');Using a static code or a code mask without replacement asterisks, you can end up in an infinite loop when trying to create multiple vouchers or a voucher which already exists.
To prevent this, an exception will be thrown after a calculated number of attempts depending on the amount of asterisks and the replacement characters.
try {
$vouchers = Vouchers::withCode('FIXED-CODE')->create(5);
} catch (FrittenKeeZ\Vouchers\Exceptions\InfiniteLoopException $e) {
// Infinite loop detected when trying to create vouchers.
}During events Voucher::$redeemer will be set to the active redeemer (FrittenKeeZ\Vouchers\Models\Redeemer).
By default vouchers will be marked as redeemed after one use, which is not always the desired outcome.
To allow a voucher to be redeemed multiple times, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::shouldMarkRedeemed() event.
Voucher::shouldMarkRedeemed(function (Voucher $voucher) {
// Do some fancy checks here.
return false;
});To prevent a voucher from being redeemed altogether, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::redeeming() event.
Voucher::redeeming(function (Voucher $voucher) {
// Do some fancy checks here.
return false;
});To prevent a voucher from being redeemed by anyone but the related user.
Voucher::redeeming(function (Voucher $voucher) {
return $voucher->redeemer->redeemer->is($voucher->owner);
});
/* ... */
$voucher = Vouchers::withOwner($user)->create();
Vouchers::redeem($voucher->code, $user);To perform additional actions after a voucher has been redeemed, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::redeemed() event.
Voucher::redeemed(function (Voucher $voucher) {
// Do some additional stuff here.
});To prevent a voucher to from being marked as unredeemed after first unredeeming, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::shouldMarkUnredeemed() event. Note that a voucher will still be marked as unredeemed if there are no more redeemers left.
Voucher::shouldMarkUnredeemed(function (Voucher $voucher) {
// Do some fancy checks here.
return false;
});To prevent a voucher from being unredeemed altogether, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::unredeeming() event.
Voucher::unredeeming(function (Voucher $voucher) {
// Do some fancy checks here.
return false;
});To perform additional actions after a voucher has been unredeemed, subscribe to the FrittenKeeZ\Vouchers\Models\Voucher::unredeemed() event.
Voucher::unredeemed(function (Voucher $voucher) {
// Do some additional stuff here.
});For convenience we provide some traits for fetching vouchers and redeemers for related entities, fx. users.
FrittenKeeZ\Vouchers\Concerns\HasRedeemers
// Associated redeemers relationship.
HasRedeemers::redeemers(): MorphMany;
// Get all associated redeemers.
$redeemers = $user->redeemers;FrittenKeeZ\Vouchers\Concerns\HasVouchers
// Owned vouchers relationship.
HasVouchers::vouchers(): MorphMany;
// Get all owned vouchers.
$vouchers = $user->vouchers;
// Associated vouchers through VoucherEntity relationship.
HasVouchers::associatedVouchers(): MorphToMany;
// Get all associated vouchers.
$vouchers = $user->associatedVouchers;
// Associated voucher entities relationship.
HasVouchers::voucherEntities(): MorphMany;
// Get all associated voucher entities.
$entities = $user->voucherEntities;You can also create vouchers owned by an entity using these convenience methods.
HasVouchers::createVoucher(Closure(FrittenKeeZ\Vouchers\Vouchers)|null $callback = null): object;
// Without using callback.
$voucher = $user->createVoucher();
// With using callback.
$voucher = $user->createVoucher(function (FrittenKeeZ\Vouchers\Vouchers $vouchers) {
$vouchers->withPrefix('USR');
});
HasVouchers::createVouchers(int $amount, Closure(FrittenKeeZ\Vouchers\Vouchers)|null $callback = null): object|array;
// Without using callback.
$vouchers = $user->createVouchers(3);
// With using callback.
$vouchers = $user->createVouchers(3, function (FrittenKeeZ\Vouchers\Vouchers $vouchers) {
$vouchers->withPrefix('USR');
});Check whether a voucher code is redeemable without throwing any errors.
Vouchers::redeemable(string $code, Closure(FrittenKeeZ\Vouchers\Models\Voucher)|null $callback = null): bool;
// Without using callback.
$valid = Vouchers::redeemable('123-456-789');
// With using callback.
$valid = Vouchers::redeemable('123-456-789', function (FrittenKeeZ\Vouchers\Models\Voucher $voucher) {
return $voucher->hasPrefix('foo');
});Check whether a voucher code is unredeemable without throwing any errors.
Vouchers::unredeemable(string $code, Closure(FrittenKeeZ\Vouchers\Models\Voucher)|null $callback = null): bool;
// Without using callback.
$valid = Vouchers::unredeemable('123-456-789');
// With using callback.
$valid = Vouchers::unredeemable('123-456-789', function (FrittenKeeZ\Vouchers\Models\Voucher $voucher) {
return $voucher->hasPrefix('foo');
});Check whether a voucher code exists, optionally also checking a provided list.
Vouchers::exists(string $code, array $codes = []): bool;
$exists = Vouchers::exists('123-456-789', ['987-654-321']);Additional helpers methods on Voucher model.
// Whether voucher has prefix, optionally specifying a separator different from config.
Voucher::hasPrefix(string $prefix, string|null $separator = null): bool;
// Whether voucher has suffix, optionally specifying a separator different from config.
Voucher::hasSuffix(string $suffix, string|null $separator = null): bool;
// Whether voucher is started.
Voucher::isStarted(): bool;
// Whether voucher is expired.
Voucher::isExpired(): bool;
// Whether voucher is redeemed.
Voucher::isRedeemed(): bool;
// Whether voucher is redeemable.
Voucher::isRedeemable(): bool;
// Whether voucher is unredeemable.
Voucher::isUnredeemable(): bool;For convenience we also provide Voucher scopes matching the helper methods.
// Scope voucher query to a specific prefix, optionally specifying a separator different from config.
Voucher::withPrefix(string $prefix, string|null $separator = null);
// Scope voucher query to exclude a specific prefix, optionally specifying a separator different from config.
Voucher::withoutPrefix(string $prefix, string|null $separator = null);
// Scope voucher query to a specific suffix, optionally specifying a separator different from config.
Voucher::withSuffix(string $suffix, string|null $separator = null);
// Scope voucher query to exclude a specific suffix, optionally specifying a separator different from config.
Voucher::withoutSuffix(string $suffix, string|null $separator = null);
// Scope voucher query to started vouchers.
Voucher::withStarted();
// Scope voucher query to unstarted vouchers.
Voucher::withoutStarted();
// Scope voucher query to expired vouchers.
Voucher::withExpired();
// Scope voucher query to unexpired vouchers.
Voucher::withoutExpired();
// Scope voucher query to redeemed vouchers.
Voucher::withRedeemed();
// Scope voucher query to without redeemed vouchers.
Voucher::withoutRedeemed();
// Scope voucher query to redeemable vouchers.
Voucher::withRedeemable();
// Scope voucher query to without redeemable vouchers.
Voucher::withoutRedeemable();
// Scope voucher query to unredeemable vouchers.
Voucher::withUnredeemable();
// Scope voucher query to without unredeemable vouchers.
Voucher::withoutUnredeemable();
// Scope voucher query to have voucher entities, optionally of a specific type (class or alias).
Voucher::withEntities(string|null $type = null);
// Scope voucher query to specific owner type (class or alias).
Voucher::withOwnerType(string $type);
// Scope voucher query to specific owner.
Voucher::withOwner(Illuminate\Database\Eloquent\Model $owner);
// Scope voucher query to no owners.
Voucher::withoutOwner();Running tests can be done either through composer, or directly calling the Pest binary.
composer test
./vendor/bin/pest --parallelIt is also possible to run the tests with coverage report.
composer test-coverage
./vendor/bin/pest --parallel --coverageThe MIT License (MIT). Please see License File for more information.