›
›
›
  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.

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.

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 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

$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.

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

The package locks the row and checks both calls against its current state in the database:

SituationResult
A different model answersNotTheAssignee thrown
The offer was already acceptedOfferNotOpen thrown
The offer was already declinedOfferNotOpen thrown
expires_at has passedOfferNotOpen 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().

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

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 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:

  • 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 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
}

Events are dispatched after commit

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.

What it does not do

  • Authorize the caller. These are ordinary method calls. Your application checks whether this user may assign this enquiry before it makes 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