›
byrcsc/laravel-assignment · 1.x
A profile holds the candidates, policy, scope, and mode for one assignable model, so assignment takes no arguments.
The fluent builder needs a candidate list at the call site. That works in a controller and fails everywhere else: a queued job, a scheduled command, or the queue flush has no chain to build.
A profile is a class that answers those questions for one assignable model. Once it is registered, assignment takes no arguments:
$enquiry->autoAssign();
$callout->autoAssign('crew');Two methods are required. The rest have defaults.
use ByRcsc\LaravelAssignment\AssignmentProfile;
use ByRcsc\LaravelAssignment\Policies\RoundRobin;
use Illuminate\Database\Eloquent\Model;
final class EnquiryProfile extends AssignmentProfile
{
public function candidates(Model $assignable, ?string $role): iterable
{
return Tradie::query()->where('available', true)->get();
}
public function policy(Model $assignable, ?string $role): string
{
return RoundRobin::class;
}
public function scope(Model $assignable, ?string $role): ?string
{
return 'trade:'.$assignable->trade;
}
}candidates() returns any iterable of persisted Eloquent models. policy()
returns a class name, a policy instance, or a closure, which is the same set
using() accepts.
Every method receives the assignable and the role, so one profile can serve
several roles on the same model by branching on $role.
The contract types the assignable as
Model. The engine does not know which model a profile was registered for. Narrow it yourself when you need the model's own properties, either with aninstanceofcheck or by accepting the wider type and failing loudly.
// config/assignment.php
'profiles' => [
App\Models\Enquiry::class => App\Assignment\EnquiryProfile::class,
App\Models\Callout::class => App\Assignment\CalloutProfile::class,
],The key is the assignable class, the value is the profile class name. Profiles are resolved through the container, so a profile can take constructor dependencies.
Resolution walks the class and then its parents, so a profile registered against a base class serves every subclass that has no profile of its own.
autoAssign() on a model with no registered profile throws NoProfile.
| Method | Default | Controls |
|---|---|---|
scope() | null | The rotation scope; null uses the derived one |
mode() | Mode::Assign | Whether to assign outright or offer |
offerTtl() | null | Seconds an offer stands; null never expires |
queueWhenEmpty() | true | Queue the assignable when nobody is available |
queuePriority() | 0 | Its position in the queue, highest first |
use ByRcsc\LaravelAssignment\Enums\Mode;
public function mode(Model $assignable, ?string $role): Mode
{
return $role === 'crew' ? Mode::Offer : Mode::Assign;
}
public function offerTtl(Model $assignable, ?string $role): ?int
{
return 120;
}Profile-driven offers are the ones that cascade. An offer created through the fluent builder has no cascade, so it ends when it is declined or expires. See offer cascades.
public function queueWhenEmpty(Model $assignable, ?string $role): bool
{
return true;
}
public function queuePriority(Model $assignable, ?string $role): int
{
return $assignable->emergency ? 10 : 0;
}With queueWhenEmpty() returning true, an autoAssign() that finds no
candidates parks the assignable rather than doing nothing. Return false and
autoAssign() returns null and leaves no trace. See the queue.
candidates(), scope(), policy(), and mode() are read.queueWhenEmpty() is true, the assignable is
queued at queuePriority().The return value is the assignment, or null when nobody was selected.
The facade takes the same path for a model you hold but whose trait you do not want to call through:
use ByRcsc\LaravelAssignment\Facades\Assignment;
Assignment::autoAssign($enquiry);
Assignment::autoAssign($callout, 'crew');candidates() runs on every call, which is what lets a
flush minutes later see a tradie who has since become available.