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— resolvesapprover_idsdirectly against the user model.role— resolvesapprover_rolevia spatie/laravel-permission'srole()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.