---
title: "Fluent builders — Approvals for Laravel | Roundly"
description: "PendingApproval and PendingApprovalRequest in practice — pinning to a request, the approvals() helper, absolute expiry and incomplete builders."
url: https://roundly-consulting.com/open-source/docs/approvals-for-laravel/fluent-builder
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Approvals for Laravel](https://roundly-consulting.com/open-source/docs/approvals-for-laravel.md)

# Fluent builders

Approvals::for(), as() and request() return small builders that collect context and write nothing until a terminal call — approve(), reject(), ask(), cancel(), toggle(), close() or open(). Every terminal goes through ApprovalsManager, so Approvals::fake() records it. The method tables are under The Approvals facade.

## Pinning a decision to one request

By default a decision joins the approvable’s latest open request. within() pins it to a specific one — which must belong to the approvable and still be open, so a decision on one model never counts towards another model’s request and nothing lands in a finished round:

```php
use RoundlyConsulting\Approvals\Facades\Approvals;

$request = Approvals::request($release)->from([$lead, $qa])->open();

Approvals::for($release)->as($lead)->within($request)->because('Ship it')->approve();

Approvals::for($otherRelease)->as($lead)->within($request)->approve();
// InvalidApprovalRequestException — the request belongs to another subject; nothing is written

// …later, once $request has resolved:
Approvals::for($release)->as($qa)->within($request)->reject();
// ClosedApprovalRequestException — its round is over; nothing is written
```

## The approvals() helper

A global approvals() helper resolves the same singleton manager — handy where you prefer a function to a facade:

```php
approvals()->for($deployment)->as($user)->approve();   // the same manager as the facade

Approvals::as($user)->for($deployment)->approve();     // start from either side
```

## Absolute expiry

expiresIn() takes seconds from now; expiringAt() takes any Carbon instance. Both exist on the decision builder, the request builder and — since 1.2.0 — the workflow builder:

```php
use Carbon\CarbonImmutable;
use RoundlyConsulting\Approvals\Facades\Approvals;

Approvals::for($budget)->as($cfo)
    ->because('Within the quarterly limit')
    ->expiringAt(CarbonImmutable::parse('2026-12-31 23:59'))
    ->approve();
```

## Incomplete builders

Decision terminals need both an actor and an approvable. A missing one throws IncompletePendingApprovalException; hasPending() and close() only need the approvable — and close() refuses an actor:

```php
use RoundlyConsulting\Approvals\Facades\Approvals;

Approvals::for($deployment)->approve();
// IncompletePendingApprovalException: No actor was set on the pending approval. Call as() first.
```

## Settings a call has no place for

Every setter stores something, but not every terminal has a place for it. Rather than drop a setting silently, a terminal that can’t use it throws InvalidApprovalRequestException before anything is written — so Approvals::fake() records nothing either:

| Call | Takes | Refuses |
| --- | --- | --- |
| `approve()` | because(), weight(), the expiry, within() | nothing |
| `reject()` | because(), weight(), within() | the expiry (expiryOnReject) — a rejection stands until it is withdrawn or superseded; withdraw it with cancel() when it should stop counting |
| `ask()` | because() (the ask’s reason), the expiry (its reply-by deadline), within() | weight() (weightOnAsk) — a pending decision counts towards no threshold; set the weight on the approve() or reject() that answers the ask |
| `cancel()` | because() (the withdrawal’s reason), within() | weight() and the expiry (settingsOnCancel, naming each one set) — a withdrawal records no decision |
| `close()` | within() (only that round) | because() (reasonOnClose) — keep why you closed the round on your own model; then as(), weight() and the expiry (settingsOnClose) — closing records no decision, involves no actor and runs no gate |
| `toggle()` | on: as approve(); off: because(), as cancel() | nothing — toggling off ignores weight() and the expiry, because one call may go either way |

```php
use RoundlyConsulting\Approvals\Facades\Approvals;

// Taken: an ask's reason and its reply-by deadline
Approvals::for($post)->as($reviewer)->because('Check the sources')->expiresIn(86400)->ask();

// Refused with InvalidApprovalRequestException — nothing is written
Approvals::for($post)->as($reviewer)->weight(2)->ask();            // weightOnAsk: set the weight on the answer
Approvals::for($post)->as($reviewer)->expiresIn(3600)->reject();   // expiryOnReject: a rejection has no expiry
Approvals::for($post)->as($reviewer)->weight(2)->cancel();         // settingsOnCancel: a withdrawal records no decision
Approvals::for($purchase)->because('Withdrawn')->close();          // reasonOnClose: a round keeps no reason
Approvals::for($purchase)->as($admin)->close();                    // settingsOnClose: closing involves no actor
```

because() is checked first, so close() with a reason and other settings throws reasonOnClose; the builder’s actor and approvable are checked before either (IncompletePendingApprovalException). because(null) sets no reason, and a bare call works as it always did. These refusals arrived in 1.2.2 (ask()’s weight, close()’s reason) and 1.2.3 (the rest) — before, the settings were dropped silently, so code that set one now gets an exception: drop the setting, which never applied.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/approvals-for-laravel.md)
