ovac/guardrails

Operational approvals engine for Laravel applications.

Fund package maintenance!
ovac4u

v1.0.0 2025-09-19 22:36 UTC

This package is auto-updated.

Last update: 2025-09-20 00:04:30 UTC


README

              Follow me anywhere @ovac4u                         | GitHub
              _________                          _________       | Twitter
             |   ___   |.-----.--.--.---.-.----.|  |  |.--.--.   | Facebook
             |  |  _   ||  _  |  |  |  _  |  __||__    |  |  |   | Instagram
             |  |______||_____|\___/|___._|____|   |__||_____|   | Github + @ovac
             |_________|                        ovac.github.io   | Facebook + @ovacposts

Total Downloads License Tests PHP Laravel

What is Guardrails?

Guardrails is an operational approvals engine for Laravel. Capture every high‑risk change, route it through the right reviewers, and apply the update only after the required signatures land. You decide which attributes are guarded, who can approve, and how many voices it takes to ship a change.

Why teams reach for Guardrails

  • Protect critical workflows – turn dangerous writes into reviewable approval requests without rewriting business logic.
  • Model or controller first – opt in with an Eloquent trait or intercept at the edge of your HTTP layer.
  • Fluent, composable flows – stack steps, mix roles and permissions, count initiators, and codify escalation in code.
  • Complete toolkit – ships with migrations, API routes, reviewer UI, and exhaustive docs so you can go live quickly.
  • Laravel-native – PHP 8.1+, Laravel 10/11, works with Spatie permissions or Sanctum token abilities out of the box.

How it fits together

  1. Mark attributes as guarded or intercept controller payloads.
  2. Use Flow::make() / FlowBuilder to describe who can sign each step.
  3. Guardrails persists the request (requests → steps → signatures) and emits events as people approve or reject.
  4. Once the final step meets its threshold, Guardrails applies the captured changes to your model for you.

Quickstart

Online Docs & Tools

Install via Composer:

composer require ovac/guardrails
  1. Register the service provider (if not auto‑discovered):
// config/app.php
OVAC\Guardrails\GuardrailsServiceProvider::class,
  1. Publish assets:
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-config
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-migrations
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-views
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-assets
php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-docs
  1. Run migrations:
php artisan migrate

60‑Second Example

Guard a model and require a quick two‑man rule (initiator + one peer):

use OVAC\Guardrails\Concerns\Guardrail;
use OVAC\Guardrails\Services\Flow;

class Post extends Model
{
    use Guardrail;

    public function guardrailAttributes(): array
    {
        return ['published'];
    }

    public function guardrailApprovalDescription(array $dirty, string $event): string
    {
        return 'Publish flag changes require editorial approval.';
    }

    public function guardrailApprovalFlow(array $dirty, string $event): array
    {
        return [
            Flow::make()
                ->anyOfPermissions(['content.publish'])
                ->includeInitiator(true, true)
                ->signedBy(2, 'Editorial Review')
                ->build(),
        ];
    }
}

When the initiator supplies a justification (for example through a form field), pass it into the guardrails context before saving:

$post->guardrails()
    ->description($request->input('approval_description'))
    ->meta(['reason_code' => $request->input('reason_code')]);

$post->fill($request->validated())->save();

Prefer controllers? Intercept without touching models:

$result = $this->guardrailIntercept($post, ['published' => true], [
    'description' => 'Editorial approval required before publishing.',
    'only' => ['published'],
    'extender' => Flow::make()->anyOfRoles(['editor','managing_editor'])->signedBy(1, 'Editorial Approval'),
]);

Why Teams Use It

  • Approval confidence for critical data while keeping code changes small.
  • Human‑readable flow rules with real‑world patterns (two‑man rule, peer review, escalation).
  • Works with your auth today — Spatie permissions if present, token abilities otherwise.

Use Cases (with examples)

  1. Publish a blog post — one more editor must approve
Flow::make()
  ->anyOfPermissions(['content.publish'])    // any editor with publish permission
  ->includeInitiator(true, true)            // author counts as one approval
  ->signedBy(2, 'Editorial Review')          // needs one more editor
  ->build();
  1. Delete a user account — two steps, different roles
Flow::make()
  ->anyOfRoles(['support_lead'])             // support lead approves first
  ->signedBy(1, 'Support Approval')
  ->anyOfRoles(['security_officer'])         // then security approves
  ->signedBy(1, 'Security Approval')
  ->build();
  1. Refund an order — one of finance OR operations
Flow::make()
  ->anyOfRoles(['finance_manager','ops_manager'])
  ->signedBy(1, 'Management Approval')
  ->build();
  1. Sensitive setting change — peer with same permission must co‑sign
Flow::make()
  ->permissions(['settings.update'])       // list all required permissions
  ->requireAnyPermissions()                // switch to any‑of
  ->samePermissionAsInitiator(true)        // peer must share at least one
  ->includeInitiator(true, true)           // initiator pre‑approved
  ->signedBy(2, 'Peer Review')
  ->build();
  1. Multi‑step escalation — ops first, then execs
Flow::make()
  ->anyOfPermissions(['ops.change'])
  ->includeInitiator(true, true)
  ->signedBy(2, 'Ops Review')
  ->anyOfRoles(['cto','cfo'])
  ->signedBy(1, 'Executive Sign‑off')
  ->build();

API (3 endpoints)

  • GET /{route_prefix} — list pending approval requests with steps/signatures.
  • POST /{route_prefix}/{request}/steps/{step}/approve — approve a step.
  • POST /{route_prefix}/{request}/steps/{step}/reject — record a rejection signature.

UI

A minimal page at /{page_prefix} consumes the API for reviewers. Publish and customize the Blade view.

Docs

  • Start here: Documentation Index
  • Or publish locally: php artisan vendor:publish --provider="OVAC\\Guardrails\\GuardrailsServiceProvider" --tag=guardrails-docs (to docs/guardrails).

Highlights worth reading next:

How It Works (Data Flow)

flowchart TD
  Start[Change attempted] --> Guard{Guardrails guard?}
  Guard -- No --> ApplyNow[Apply change immediately]
  Guard -- Yes --> Capture[Capture request and steps]
  Capture --> Review{Reviewer decision}
  Review -- Approve --> Apply[Apply new data]
  Review -- Reject --> Halt[Reject request]
  Apply --> Done[Emit completion events]
  Halt --> Done
Loading

Keep approvals close to where changes happen (models) or intercept in controllers. Steps define who can sign and how many signatures you require.

Extending Migrations and Models

  • Add columns to the published migrations (e.g., reason, category, workspace_id) with a new migration; the package models use $guarded = [], so new attributes are writable.
  • If you need JSON casting or extra relations, create an app model that extends the package model:
// app/Models/ApprovalRequest.php
namespace App\Models;

class ApprovalRequest extends \OVAC\Guardrails\Models\ApprovalRequest
{
    protected $casts = [
        'meta' => 'array',
        'reason' => 'string',
    ];

    public function workspace()
    {
        return $this->belongsTo(Workspace::class);
    }
}

Use events like ApprovalRequestCaptured to populate new columns or meta. See Config Recipes and Auditing & Changelog.

Search keywords: "laravel approval workflow", "laravel multi signature approvals", "human in the loop approvals", "laravel model guarded changes", "laravel approval steps thresholds", "spatie permissions approval flow", "controller intercept approvals", "two-man rule laravel".

Documentation Website

Guardrails ships build scripts instead of doc sources so the Composer install stays slim. The GitHub Pages deployment uses scripts/docs-site/build-docs-site.js to assemble a Docusaurus bundle from resources/docs on demand.

  • Generate a local bundle with node scripts/docs-site/build-docs-site.js ./build/docs-site (requires Node 20+). When previewing locally, open http://localhost:3000/guardrails/ after running npx serve ./build/docs-site. For a root-based preview, run DOCS_BASE_URL=/ node scripts/docs-site/build-docs-site.js ./build/docs-site-local && npx serve ./build/docs-site-local.
  • Configure optional environment variables (DOCS_SITE_URL, DOCS_BASE_URL, DOCS_REPOSITORY_URL, DOCS_PACKAGIST_URL) to tune canonical URLs and metadata. Defaults infer the correct GitHub Pages paths from the current repository.
  • The published site exposes /playground and /assistant routes for the interactive flow builder and BYO-key AI chat, both running entirely in the browser.
  • Tags that start with v automatically generate frozen documentation snapshots so visitors can browse historical releases.
  • .github/workflows/docs-site.yml runs on every push and tag, rebuilding the static site and deploying it to GitHub Pages (gh-pages branch).

Tests & Coverage

Run the full suite with Pest (quiet mode by default):

composer test

Generate coverage locally when you need instrumentation or Clover output for CI:

composer test:coverage
composer test:ci # emits coverage.xml

See the Full Testing Guide for environment setup, Laravel Testbench tips, and additional testing recipes.

Support

If this package saves you time, please consider: