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

Selection policies.

You pass the candidates, a policy picks one of them, and four policies ship with the package.

A policy answers one question: given these candidates, which one 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();

The shipped policies

PolicyPicksState
RoundRobinThe next candidate after the last one this scope tookCursor per scope
LeastWorkloadThe candidate with the fewest open assignmentsNone
RandomAny candidateNone
FirstAvailableThe first candidate in the order you passed themNone
Any closureWhatever you returnYours

using() takes a class name, an instance, or a closure. A class name is resolved through the container, so a policy of your own can take constructor dependencies.

RoundRobin

Rotation is anchored to the last assignee this scope picked, not to a position in the array. The policy finds that assignee in the current list and takes the next one, wrapping at the end. When the previous assignee is no longer in the list, it starts from the front.

That anchoring is what makes rotation survive a candidate list you rebuild on every call. Reordering, adding, and removing candidates between calls all behave. See scopes and rotation.

LeastWorkload

Workload is the count of open assignments: active rows plus offers nobody has answered. Counting offers is deliberate, so ten pending offers cannot pile onto one idle crew.

use ByRcsc\LaravelAssignment\Policies\LeastWorkload;

->using(LeastWorkload::class)                    // every open assignment counts
->using(LeastWorkload::of(Callout::class))      // only callouts count

of() constrains the count to one assignable type, so a crew's open support tickets do not make them look busy for callouts.

Ties break toward the candidate assigned least recently, and a candidate who has never been assigned wins over one who has. Equal workloads therefore still rotate rather than always landing on 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.

FirstAvailable

Takes the first candidate in the order you passed them, which makes your query order the priority order. Senior tradies first is orderBy('grade') and nothing else.

Random

Picks uniformly at random using random_int().

Closures

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. That is a useful way to say "none of these are good enough right now" and let the queue hold the work.

Returning a model that is not in the candidate list is an error, and raises an UnexpectedValueException. A policy cannot smuggle in an assignee the application did not offer.

Writing a policy class

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, row locked for the duration of the selection. Use it when your policy needs to remember something between calls. See scopes and rotation.

Eligibility is yours

The list deliberately contains 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.

What it does not do

  • Filter candidates. A policy chooses; it does not reject. Filtering happens in your query.
  • Retry with a different candidate. One call selects once. Passing an assignee whose slot is taken raises SlotOccupied, and any rotation cursor moved during that attempt is rolled back with the transaction.
  • Look at assignment history beyond workload. LeastWorkload reads counts and last-assigned timestamps. Anything richer belongs in a closure or a policy class of your own.

What to read next

  • Scopes and rotation for where stateful policies keep their cursor.
  • Profiles to move the candidates and the policy off the call site.
  • API reference for the exact contract signature.
PreviousAssigning and offeringNextScopes and rotation
View source

On this page

  1. The shipped policies
  2. RoundRobin
  3. LeastWorkload
  4. FirstAvailable
  5. Random
  6. Closures
  7. Writing a policy class
  8. Eligibility is yours
  9. What it does not do
  10. What to read next