›
byrcsc/laravel-assignment · 1.x
A selection policy picks one assignee from the candidates you pass in.
A policy decides which of the given candidates gets the work. It never asks whether a candidate should have been in the list.
use ByRcsc\LaravelAssignment\Facades\Assignment;
use ByRcsc\LaravelAssignment\Policies\RoundRobin;
Assignment::for($enquiry)
->among(Tradie::query()->where('available', true)->get())
->using(RoundRobin::class)
->assign();| Policy | Picks | State |
|---|---|---|
RoundRobin | The next candidate after the last one this scope took | Cursor per scope |
LeastWorkload | The candidate with the fewest open assignments | None |
Random | Any candidate | None |
FirstAvailable | The first candidate in the order you passed them | None |
| Any closure | Whatever you return | Yours |
using() takes a class name, an instance, or a closure. The container
resolves a class name, so a policy of your own can take constructor
dependencies.
Rotation starts from the last assignee this scope picked, not from a position in the array. The policy finds that assignee in the current list and takes the next one. After the last candidate, it goes back to the first. When the previous assignee is no longer in the list, the policy starts from the front.
Because the policy tracks the last assignee instead of a position, rotation keeps working when you rebuild the candidate list on every call. Reordering, adding, and removing candidates between calls do not break the rotation. See scopes and rotation.
Workload is the count of open assignments: active rows plus offers nobody has answered. The policy counts offers on purpose, so one idle crew cannot collect ten pending offers.
use ByRcsc\LaravelAssignment\Policies\LeastWorkload;
->using(LeastWorkload::class) // every open assignment counts
->using(LeastWorkload::of(Callout::class)) // only callouts countof() constrains the count to one assignable type, so a crew's open support
tickets do not make them look busy for callouts.
On a tie, the policy picks the candidate assigned least recently. A candidate who has never been assigned wins over one who has. Equal workloads therefore still rotate instead of always going to the same person.
The policy issues a fixed number of queries whatever the size of the candidate list: one grouped count and one grouped maximum, both constrained to the candidates you passed.
The policy takes the first candidate in the order you passed them, so your
query order is the priority order. To put senior tradies first, add
orderBy('grade') to the query.
The policy picks a candidate uniformly at random with random_int().
A closure receives the assignable and the candidates, and returns one candidate or null:
->using(fn ($enquiry, $candidates) => $candidates->sortByDesc('rating')->first())Returning null selects nobody, and the call returns null rather than throwing. Return null to say "none of these are good enough right now" and let the queue hold the work.
If a policy returns a model that is not in the candidate list, the call throws
UnexpectedValueException. A policy cannot add an assignee that the
application did not pass in.
use ByRcsc\LaravelAssignment\Contracts\SelectionPolicy;
use ByRcsc\LaravelAssignment\ScopeState;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
final class HighestRated implements SelectionPolicy
{
public function select(Model $assignable, Collection $candidates, ScopeState $state): ?Model
{
return $candidates->sortByDesc('rating')->first();
}
}The third argument is the scope's own key-value store. The package locks the scope row for the duration of the selection. Use the store when your policy needs to remember something between calls. See scopes and rotation.
By design, the package ships no territory policy and no trade policy. Who can take an assignment is a query on data your application already owns. Who gets it, among those, is what a policy decides.
Routing by suburb is therefore a query plus a scope, not a feature:
Assignment::for($callout)
->among(Crew::query()
->where('suburb', $callout->suburb)
->where('available', true)
->get())
->role('crew')
->scope('suburb:'.$callout->suburb)
->using(LeastWorkload::class)
->assign();Per-assignee capacity caps work the same way. Filter by
$crew->workload() < 3 before passing the candidates in, and no policy needs
to know the cap exists.
SlotOccupied. The transaction rolls back
any rotation cursor that moved during that attempt.LeastWorkload reads counts
and last-assigned timestamps. Anything richer belongs in a closure or a policy
class of your own.