›
byrcsc/laravel-assignment · 1.x
assign() and offer() create assignments, and only the assignee an offer names can answer it.
assign() writes an active assignment straight away. offer() writes one the
assignee has to accept or decline. Both hold the slot, and both refuse to fill
a slot that is already occupied.
Use assign() when the application makes the decision, and offer() when the
assignee gets a say: a crew who can be busy, a contractor who can
turn work down.
$assignment = $enquiry->assign($tradie);
$assignment = $callout->assign($crew, role: 'crew');
$assignment = $enquiry->assign($tradie, by: $manager);The assignment is active from the first moment. The optional by argument
takes any model, and the package records it in the polymorphic assigned_by
columns. Leave it off and assigned_by stays null. A null assigned_by means
the engine made the assignment.
Assigning into an occupied slot throws SlotOccupied, so filling a slot is
never accidental.
A role is a non-empty string, or null for the slot with no role. An empty
string throws InvalidArgumentException.
$offer = $callout->offer($crew, role: 'crew', ttlSeconds: 120);The package sets the row to offered, stamps offered_at, and sets
expires_at to offered_at plus the TTL. Leave ttlSeconds off and the offer
never expires on its own. It stays open until somebody answers it. A TTL below
one second throws InvalidArgumentException.
While the offer is open, it occupies the slot. Another assign() or offer()
on that slot throws. After expires_at passes, the next assign() or offer()
on the slot ends the old offer with reason expired and dispatches
OfferExpired for it.
The answer goes through the assignee, not through the assignment, because only the assignee an offer names may answer it:
$crew->accept($offer); // offered becomes active, accepted_at is stamped
$crew->decline($offer); // the row ends with reason declinedThe package locks the row and checks both calls against its current state in the database:
| Situation | Result |
|---|---|
| A different model answers | NotTheAssignee thrown |
| The offer was already accepted | OfferNotOpen thrown |
| The offer was already declined | OfferNotOpen thrown |
expires_at has passed | OfferNotOpen thrown |
If an accept and a decline run at the same time, exactly one succeeds. The
other gets OfferNotOpen.
Accepting dispatches AssignmentAccepted and then Assigned, so a listener
that reacts to work becoming active does not need to know whether it arrived
through an offer or through assign().
$enquiry->unassign(); // ends with reason unassigned
$callout->unassign(role: 'crew');
$assignment->complete(); // ends with reason completed
$assignment->complete(by: $manager);unassign() reads the open row for that slot and ends it. If the slot is
empty, unassign() throws NotAssigned.
complete() is for work that finished rather than work taken away. It applies
only to an active assignment. Calling it on an offered or already ended row
throws InvalidTransition.
Replacing a holder is its own verb, so it cannot happen by accident:
$callout->reassign($otherCrew, role: 'crew');Reassignment is atomic. Inside one transaction, the package ends the open row
with reason reassigned and creates the replacement row. If the package cannot
write the replacement, the old row stays open.
reassign() refuses these cases:
NotAssigned.AlreadyAssigned.Reassignment works on an open offer as well as an active assignment, so you can take back an offer nobody has answered yet.
Reassignment dispatches Reassigned, and the event includes both rows:
public function handle(Reassigned $event): void
{
$event->oldAssignment; // the row that just ended
$event->newAssignment; // the row that replaced it
}Every lifecycle event implements ShouldDispatchAfterCommit. If the
surrounding transaction rolls back, Laravel dispatches no event, so a listener
never sees an assignment that does not exist. See events.