3neti/voucher
3neti/voucher is a Laravel/PHP package for generating, managing, and redeeming vouchers. It provides voucher code handling, validation, and Eloquent-friendly integration, making it easier to add promotional, discount, or redemption workflows to your application.
This package models voucher contracts as data — validation requirements (signature, selfie, OTP, face match, time window, location) are represented as *InstructionData DTOs that you attach to a voucher definition. Start by reading the ValidationInstructionData class first: it is the aggregator that tells you which DTO types exist and how they are composed, so it doubles as the map of the whole validation engine.
Minimal first use case — build a voucher contract with a time-bound, location-gated requirement:
use YourPackage\Data\TimeValidationInstructionData;
use YourPackage\Data\LocationValidationInstructionData;
use YourPackage\Data\ValidationInstructionData;
$instructions = new ValidationInstructionData(
time: new TimeValidationInstructionData(
startsAt: now()->addHour(),
endsAt: now()->addHours(3),
),
location: new LocationValidationInstructionData(
latitude: 14.5995,
longitude: 120.9842,
radiusMeters: 150,
),
);
Two things to note on day one:
*InstructionData names, not the legacy ones. As of 0.10.3 the canonical DTOs are TimeValidationInstructionData and LocationValidationInstructionData, matching the pre-existing SignatureValidationInstructionData, SelfieValidationInstructionData, OtpValidationInstructionData, and FaceMatchValidationInstructionData. The old TimeValidationData / LocationValidationData classes still exist but are deprecated shims.Construct ValidationInstructionData from the canonical DTOs. The aggregator was updated in 0.10.3 to use the new names internally, so you can rely on instanceof TimeValidationInstructionData checks and on ::class comparisons throughout your own code without the shim layer getting in the way.
Type-hint against the new classes, not the old ones. Any factory, form request, action, or service that produces a time or location instruction should declare the *InstructionData type. This keeps static analysis and IDE navigation clean and means you are never depending on a class that is explicitly marked for removal.
final class BuildVoucherContract
{
public function handle(array $payload): ValidationInstructionData
{
return new ValidationInstructionData(
signature: ! empty($payload['signature'])
? new SignatureValidationInstructionData(...)
: null,
time: isset($payload['starts_at'])
? TimeValidationInstructionData::from($payload)
: null,
);
}
}
Normalise at the boundary. Because both old and new class names resolve to compatible objects (the shims extend the canonical DTOs), a mixed codebase will still run. The clean pattern is to convert once — in a mapper or form request — and pass only canonical DTOs deeper into the domain. That way any legacy TimeValidationData arriving from a third-party integration or an older queued job is upgraded in a single, testable place.
Test against the contract, not the class name. Assertions like assertInstanceOf(TimeValidationInstructionData::class, $instruction) pass for both the canonical class and the shim, which is exactly what you want while integrations catch up. Reserve strict assertSame(TimeValidationInstructionData::class, $instruction::class) for tests that specifically guard the deprecation migration.
Serialize with care if you persist contracts. If you store voucher contracts as JSON and hydrate via ::from(), the payload keys are unaffected by this release, but be deliberate about which class you hydrate into — hydrating into the deprecated shim will silently keep deprecated code paths alive in your application.
TimeValidationData extends TimeValidationInstructionData means the old name is a subclass, not an alias. A method that type-hints TimeValidationData will reject a TimeValidationInstructionData instance — the covariance does not work in reverse. Audit for any type hints or instanceof checks using the old names; these are the only places the rename can actually break you.instanceof TimeValidationData is now narrower than you think. Since the canonical class does not extend the shim, code branching on the legacy class will quietly skip the new DTOs. Grep for the old class names as part of the upgrade, even though the release is marked non-breaking.TimeValidationData → TimeValidationInstructionData and LocationValidationData → LocationValidationInstructionData across use statements and type hints is a low-risk migration — but do it in a branch, and re-run your contract tests, because a naive replace will also rewrite the deprecated class definitions if the package source is vendored into your repo.*InstructionData suffix now that the full set is consistent: the aggregator and any reflection-based discovery are keyed on that convention, and mixing suffixes is how you end up with instructions that validate but are never dispatched.$instruction::class alongside the result — you will immediately see whether a legacy shim is still in the path.How can I help you explore Laravel packages today?