›
byrcsc/laravel-assignment · 1.x
Read the current holder, the workload, and the full history from either side of an assignment.
Both traits read the same assignments table from their own side. An
assignable asks who has it. An assignee asks what it is holding.
$enquiry->assignment(); // the open assignment, or null
$enquiry->openAssignment(); // the same row, longer name
$enquiry->assignee(); // the assignee model, or null
$enquiry->isAssigned(); // is the slot filled
$enquiry->queuedAssignment(); // the queue entry, if it is waiting
$enquiry->assignments; // every row, oldest firstEach of them takes an optional role, and the role-less slot is its own slot:
$callout->assignee('crew');
$callout->isAssigned('supervisor');
$callout->assignment(); // the slot with no role, likely null hereassignment() and openAssignment() are the same method under two names. Both
return the row holding the slot, which is the active assignment when there is
one, or an unanswered offer that has not expired.
assignments sorts rows oldest first by created_at and then by id, so the
relation reads as a timeline.
$crew->activeAssignments; // active rows only
$crew->openAssignments; // active plus unanswered offers
$crew->assignments; // every row this crew ever had
$crew->workload(); // how many are open
$crew->workload(Callout::class); // how many open, of one assignable typeworkload() counts the same rows as openAssignments, so an assignee holding
one callout and one unanswered offer has a workload of 2. LeastWorkload uses
the same definition, so a capacity filter you write agrees with the policy the
engine runs.
The type argument accepts a class name and honours the morph map, so it works
whether or not assignable_type stores aliases.
Unlike the assignable's relation, these are not ordered. Add your own
latest() or oldest() when order matters.
Assignment has these scopes, each usable on any of the relations above:
| Scope | Matches |
|---|---|
open() | Active, or offered with no expiry or a future expiry |
active() | Status is active |
offered() | Status is offered, whether or not it is overdue |
ended() | Status is ended |
expired() | Offered, with an expiry that has passed |
$callout->assignments()->ended()->get(); // past holders
$crew->assignments()->expired()->count(); // offers this crew let lapse
Assignment::query()->open()->count(); // open work everywhereopen() and expired() both read expires_at, so they agree with the
timestamp rather than with the stored status. offered() checks only the
stored status. Use it to find rows that are overdue but not yet ended.
$assignment->assignable; // back to the Enquiry, Callout, ...
$assignment->assignee; // the Tradie, Crew, ...
$assignment->assignedBy; // the actor, or null when the engine did it
$assignment->role; // the slot, or null
$assignment->status; // AssignmentStatus enum
$assignment->ended_reason; // EndReason enum, or null while open
$assignment->offered_at;
$assignment->accepted_at;
$assignment->expires_at;
$assignment->ended_at;
$assignment->scope; // which rotation produced itThe model casts the four timestamps to CarbonImmutable. status and
ended_reason are backed enums, so comparisons are type safe:
use ByRcsc\LaravelAssignment\Enums\EndReason;
$assignment->ended_reason === EndReason::Declined;These predicates read the state without touching the enums:
$assignment->isOpen();
$assignment->isActive();
$assignment->isOffered();
$assignment->isEnded();
$assignment->isExpired();Who currently holds work, by assignee:
use ByRcsc\LaravelAssignment\Models\Assignment;
Assignment::query()
->open()
->selectRaw('assignee_type, assignee_id, COUNT(*) as open_count')
->groupBy('assignee_type', 'assignee_id')
->get();How often offers are refused:
Assignment::query()
->ended()
->whereIn('ended_reason', [EndReason::Declined, EndReason::Expired])
->count();How long a callout waited for a crew:
$accepted = $callout->assignments()
->where('role', 'crew')
->whereNotNull('accepted_at')
->first();
$accepted?->created_at->diffInSeconds($accepted->accepted_at);assignable and assignee are morph relations. Use
with('assignee') when you are reading many rows.slot column. The column is internal, and the model hides it
from array and JSON output.