Skip to content

Approver resolution

A step names who can act on it. Turning that into a concrete set of users is the job of the approver resolver.

The default resolver

Talivio\ApprovalFlows\Support\DefaultApproverResolver handles the two built-in approver_type values:

  • users — resolves approver_ids directly against the user model.
  • role — resolves approver_role via spatie/laravel-permission's role() scope, if that method exists on your user model.
public function resolve(ApprovalStep $step): Collection
{
    $userModel = Approval::userModel();

    if ($step->approver_type === 'users') {
        return $userModel::query()
            ->whereIn('id', $step->approver_ids ?? [])
            ->get();
    }

    if ($step->approver_role !== null && method_exists($userModel, 'role')) {
        return $userModel::role($step->approver_role)->get();
    }

    return collect();
}

canAct($step, $user) simply checks whether the user is in the resolved set.

The user model

Resolution uses the model from config:

// config/approval-flows.php
'user_model' => null, // null falls back to config('auth.providers.users.model')

Set it explicitly if your approvers are a different model than your default auth user.

Writing a custom resolver

Anything beyond roles and explicit IDs — department managers, org-chart lookups, "the record's owner's supervisor" — is a custom resolver. Implement two methods and point the config at your class:

namespace App\Approvals;

use Talivio\ApprovalFlows\Models\ApprovalStep;
use Illuminate\Support\Collection;

class DepartmentResolver
{
    public function resolve(ApprovalStep $step): Collection
    {
        // Return a Collection of user models who may act on this step.
        return User::where('department', $step->approver_role)
            ->where('is_manager', true)
            ->get();
    }

    public function canAct(ApprovalStep $step, mixed $user): bool
    {
        return $this->resolve($step)->contains(
            fn ($u) => $u->getKey() === $user->getKey()
        );
    }
}
// config/approval-flows.php
'approver_resolver' => \App\Approvals\DepartmentResolver::class,

The resolver is pulled from the container (app(config('approval-flows.approver_resolver'))), so constructor injection works.

The Inbox and custom resolvers

The Approval Inbox lists pending approvals with a single SQL query (ApprovalManager::pendingForUser()) that understands explicit users, spatie roles, and delegations — it does not call your resolver row by row, for performance. If your custom resolver introduces logic the query can't express, override the inbox query so the list matches what canAct() allows. Acting on an item still always goes through your resolver's canAct(), so security is never bypassed — a mismatch only affects which items appear in the list.

Next: Delegation & escalation.