›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-assignment
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Assignments and slots
  • Assigning and offering
  • Selection policies
  • Scopes and rotation
  • Profiles
  • The queue
  • Offer cascades

Operations

  • Expiry and scheduling
  • Reading assignments
  • Notifications
  • Events and listeners
  • Concurrency guarantees

Reference

  • Configuration
  • API reference
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-assignment · 1.x

Assigning and offering.

Two verbs 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.

Reach for assign() when the decision is the application's, and for offer() when the assignee gets a say: a crew who can be busy, a contractor who can turn work down.

Assign

$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 is any model, and it is recorded in the polymorphic assigned_by columns. Leave it off and assigned_by stays null, which means the engine did it.

Assigning into an occupied slot throws SlotOccupied, so filling a slot is never accidental.

Offer

$offer = $callout->offer($crew, role: 'crew', ttlSeconds: 120);

The row is offered, offered_at is stamped, and expires_at is offered_at plus the TTL. Leave ttlSeconds off and the offer never expires on its own: it stands until somebody answers it.

While the offer stands it holds the slot. Another assign() or offer() on that slot throws.

Answering an offer

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 declined

Both are checked against the row as it is in the database at that moment, under a lock:

SituationResult
A different model answersNotTheAssignee thrown
The offer was already acceptedOfferNotOpen thrown
The offer was already declinedOfferNotOpen thrown
expires_at has passedOfferNotOpen thrown

An accept racing a decline resolves to exactly one winner. The loser 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().

Ending an assignment

$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. There is nothing to end when the slot is empty, which 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.

Reassigning

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 open row ends with reason reassigned and the replacement row is created. If the replacement cannot be written, the old row is not ended.

Two things it refuses:

  • An empty slot. There is nothing to replace, so it throws NotAssigned.
  • The assignee who already holds the slot, which throws 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 fires Reassigned, carrying both rows:

public function handle(Reassigned $event): void
{
    $event->oldAssignment;   // the row that just ended
    $event->newAssignment;   // the row that replaced it
}

Events are dispatched after commit

Every lifecycle event implements ShouldDispatchAfterCommit. Inside a transaction that later rolls back, no event is dispatched, so a listener never sees an assignment that does not exist. See events.

What it does not do

  • Authorize the caller. These are ordinary method calls. Whether this user may assign this enquiry is your application's check, before the call.
  • Reject an ineligible assignee. Pass a crew who is off shift and you get an assignment to a crew who is off shift.
  • Notify anyone but the assignee. See notifications.

What to read next

  • Selection policies to stop naming the assignee.
  • Offer cascades to pass a declined offer along automatically.
  • Exceptions for every typed failure and when it is thrown.
PreviousAssignments and slotsNextSelection policies
View source

On this page

  1. Assign
  2. Offer
  3. Answering an offer
  4. Ending an assignment
  5. Reassigning
  6. Events are dispatched after commit
  7. What it does not do
  8. What to read next