Download the PHP package spawnflow/spawnflow-laravel without Composer
On this page you can find all versions of the php package spawnflow/spawnflow-laravel. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download spawnflow/spawnflow-laravel
More information about spawnflow/spawnflow-laravel
Files in spawnflow/spawnflow-laravel
Package spawnflow-laravel
Short Description Fluent, chain-based API request lifecycle for Laravel. Spawn context, resolve subjects, gate ownership, validate, persist — in one expression.
License MIT
Homepage https://github.com/keybrdist/spawnflow-laravel
Informations about the package spawnflow-laravel
Spawnflow
Your entire API request lifecycle in one fluent chain.
Authentication, subject resolution, ownership verification, field-level permissions, validation, and persistence — one expression that reads like a sentence.
Why use this?
In conventional Laravel, adding a new API resource means creating a controller, form request, policy, resource, and wiring routes — five or more files that must agree on the same truth. Spawnflow replaces that with a single config entry and an optional context enum.
| Trait | What it means |
|---|---|
| Runtime fluent chain | The entire request lifecycle is one method chain, not spread across files |
| Dynamic subject resolution | Models resolve from a URL segment via a registry — no per-resource controllers |
| Inline authorization | Ownership and field permissions live in the chain, not in separate policy files |
| Minimal file surface | New resource = one command (spawnflow:resource --generate), or 1 config entry + 1 enum by hand |
| Reads like a sentence | spawn → auth → resolve → ask → fields → validate → save → present |
Built for LLM-assisted codebases
Spawnflow is intentionally optimized for codebases where AI writes the majority of code.
| Property | Why it matters |
|---|---|
| One pattern to repeat | An LLM doesn't need to coordinate 5 file types per resource |
| ~500 lines total surface | The entire Flow class + a context enum fits in a single context window |
Exhaustive match expressions |
PHP enums enforce every permission branch is handled — no forgotten cases |
| Minimal diff surface | Adding a resource is mechanical to generate, easy to review |
| Explicit chain, no magic | No middleware, observers, or policies to hallucinate — the chain says exactly what happens |
Installation
Publish the config:
Quick Start
The 3-command path
From an existing table to a registered, permission-aware resource:
--generate reads the table's real columns and foreign keys and writes
app/Spawnflow/PostFields.php + PostContext.php. The FieldSet carries
#[SpawnSubject('posts', ...)], so it registers itself — no config edit.
Every additional resource is one more command. Inference is make-time
only: the generated files are the canonical, editable declarations.
Deploy-time: php artisan spawnflow:cache freezes attribute discovery
(mirroring Laravel's bootstrap caches); spawnflow:clear unfreezes.
Prefer explicit config? Everything below still works — config entries override discovered ones.
1. Register subjects
Map URL segments to Eloquent models in config/spawnflow.php:
2. Use Flow in a controller
3. Add routes
Chain API
Every method returns $this (fluent) unless noted as terminal.
| Method | Signature | Description |
|---|---|---|
spawn |
spawn(Request $request): static |
Entry point. Extracts user and request context. |
auth |
auth(?string $role = null): static |
Verifies authentication. Optionally requires a role. |
resolve |
resolve(string $subject): static |
Looks up the subject alias in the registry, instantiates the model. |
ask |
ask(string $method, int\|array $ids): static |
Ownership verification. Loads the instance (single ID) or validates all IDs are owned (array). |
fields |
fields(?string $contextClass = null): static |
Resolves field-level permissions from a FieldContext enum. Auto-resolves from config if no class given. |
validate |
validate(?array $rules = null): static |
Validates request data. Uses context rules when active, or accepts explicit rules. |
save |
save(array $data): static |
Creates or updates. Strips disallowed fields when a context is active. |
delete |
delete(int\|array $ids): JsonResponse |
Terminal. Deletes record(s) by ID. |
gate |
gate(Closure $callback): static |
Arbitrary authorization. Callback receives the Flow; should throw on failure. |
after |
after(Closure $callback): static |
Post-operation hook for side effects (events, jobs, notifications). |
present |
present(?string $resourceClass = null, int $statusCode = 200): JsonResponse |
Terminal. Returns JSON response. Filters to visible fields when context is active. |
list |
list(?int $perPage = null): JsonResponse |
Terminal. Paginated listing with ownership scoping and validated sorting. |
Accessors
| Method | Returns |
|---|---|
getUser() |
?User |
getInstance() |
?Model — the loaded record (after ask() or save()) |
getSubject() |
?Model — the unhydrated model class instance |
getContext() |
?FieldContext |
getRequest() |
?Request |
Field-Level Permissions
Field-level permissions use context enums — PHP enums that encode every role+state combination as a case. Each case declares which fields are editable, what validation rules apply, and which fields are visible in responses.
Define a context enum
Scaffold one from the stub:
Then fill in the editableFields(), validation(), and visibleFields() cases:
Register it
How it works
When you call ->fields(PostContext::class):
- The enum's
resolve()inspects the user and record to pick a case (e.g.,OwnerDraft) ->validate()uses that case'svalidation()rules->save()strips any fields not ineditableFields()->present()filters the response tovisibleFields()
If the resolved case has zero editable fields (e.g., Viewer), the chain throws ForbiddenFieldAccessException immediately.
The discriminated union concept
Each context enum case is a discriminated union variant. The value string (e.g., "owner:draft") acts as the discriminator. This maps directly to TypeScript discriminated unions for frontend type safety:
Generic Controller
SpawnflowController handles CRUD for any registered subject with 4 routes:
Adding a new resource requires zero new controllers and zero new routes — just a config entry and optionally a context enum.
Field Descriptors
Field descriptors make fields type-aware. A FieldSet class per subject declares what each field is — type, widget, label, base validation rules, enum options, relation semantics — so the schema endpoint and the generator can serve frontends everything needed for form rendering and client-side validation, from one declaration.
Register it:
Contexts keep referencing fields by name; the schema layer joins names to descriptors. Subjects without a FieldSet fall back to minimal inferred descriptors.
Eligibility Rules
Context enums answer who may touch a field in what record state. Eligibility rules answer the orthogonal question: given the form's current values, is this field visible/enabled? Rules are declared on descriptors, serialized into the contract, and evaluated identically in PHP and JS — never put role checks in rules.
- Condition body is restricted JSON Logic — fixed op allowlist (
==strict,!=,>,<,>=,<=,and,or,!,in,var,missing). Unknown ops or missing vars fail closed. Conditions reference sibling field values, never other fields' eligibility — no cycles by construction. - Server stays authoritative: the resolved schema ships per-field verdicts; clients also get the rule and re-evaluate live as values change (hidden fields unmount, disabled fields reject input) without a round trip.
- Rules are never cosmetic:
save()discards rule-ineligible values (clear-on-ineligible);validate()skips their rules. - Serialization guards: referencing an undeclared field, or one a variant can't see, throws
InvalidEligibilityExceptionat declaration time — unless the field is->serverResolved(). - Cross-runtime parity is pinned by one conformance suite (
resources/conformance/eligibility-fixtures.json), run by Pest and vitest against the same fixtures.
Groups
Groups are first-class eligibility nodes — sections or wizard steps that accept the same rule envelope. A hidden group hides its members regardless of their own rules (AND-composition):
Centralized Validation
Rules live once — on the field descriptors, with per-context overrides — and every consumer enforces the same thing:
1. The chain. validate() with no arguments sources rules automatically: explicit argument → context validation() per field → field base rules → descriptor-implied rules (type checks, enum in:, relation exists:{table},{key}, nullability).
2. FormRequests. For conventional controllers, bridge into the same rules without adopting the chain:
rules() resolves the caller's context (record loaded from the route's {id}, synthetic record on create) and returns the same effective rules the chain enforces and the schema endpoint serves.
3. Live validation (Precognition). A request with a Precognition header makes validate() run validation only and halt the chain with 204 + Precognition: true (or the standard 422 on failure). Precognition-Validate-Only: title,email scopes the pass to specific fields — Laravel Precognition frontend helpers work against Spawnflow routes without duplicated rules.
Schema Endpoint
Enable the built-in schema routes to serve field schemas to your frontend:
This registers:
GET /spawnflow/schema/{subject}— descriptors + all context variants for the subjectGET /spawnflow/schema/{subject}/{id}— the resolved variant for a specific record
Responses follow the versioned schema contract v1 (docs/schema-contract.md). Validation rules are serialized structurally — mechanically compilable to Zod — with rules a client can't evaluate (database checks, closures) flagged serverOnly.
Resolved variant response:
All variants response carries descriptors once plus per-variant editable_fields, visible_fields, and effective structured rules — a discriminated union keyed by context. See docs/schema-contract.md for the full specification.
Frontend Generation
Generate TypeScript types and Zod schemas from the same contract the schema endpoint serves:
Per subject, one module containing:
PostsFields— field-map type from descriptors (enums become literal unions, relations becomenumber, nullability respected)postsFieldMeta— widgets, labels, options, relation metadata for rendererspostsOwnerDraftSchema, … — one Zod schema per context variant, compiled from the structured rulespostsSchemas/postsVariants— context-keyed maps of schemas and editable/visible field listsPostsVariant— the discriminated union over contexts (emit_unions)
Plus index.ts and an optional thin fetch client (emit_client) for SpawnflowController + schema routes.
Zod compilation is honest about its limits: rules a client can't check compile to a trailing /* server: unique */ comment, unmapped rules to /* unhandled: ... */ — nothing is silently dropped.
The generator and the live endpoint emit through one serializer — generated artifacts and API responses cannot drift.
Relation Options
Relation fields get a data source for free. When schema routes are enabled, GET /spawnflow/options/{subject}/{field}?q=&page= serves {value, label} pages from the related model's display column — ownership-scoped by default, unscoped() for shared lookups (countries, plans), q search for searchable() fields. Relation descriptors carry the options_url so renderers wire comboboxes automatically.
Live Invalidation (SSE, opt-in)
GET /spawnflow/events streams change signals whenever a subject is written through the Flow chain — invalidation only, never state. Clients refetch through the endpoints they already use, so a dropped stream degrades to non-live, never to wrong data.
since[subject]=n replays missed changes on reconnect. Each open stream holds a PHP worker — set events_max_polls to recycle idle streams (EventSource reconnects automatically).
Livewire Renderer
Server-rendered Laravel apps get the same machinery with zero JavaScript contract: ONE generic schema-interpreting component (no per-form classes), registered automatically when Livewire is installed.
- Widgets, labels, options come from the FieldSet descriptors; per-role editability from the context enum — identical to the React renderer.
- Eligibility rules re-evaluate server-side on every update (the PHP
evaluator is the only evaluator;
serverResolved()needs no special handling here). - Saves run through the same
Flowchain as the HTTP path — ownership, variant stripping, rule enforcement, validation. One write path, two renderers. - Restyle via
php artisan vendor:publish --tag=spawnflow-views.
Owning the Form Source (shadcn registry)
The renderer is also distributed as a shadcn registry item, so the
presentational source lands in your repo — restyle it, rewrite it,
let your LLM edit it. The contract, evaluator, and client stay versioned
in @spawnflow-dx/core:
Registry artifacts build from the same source as the npm package
(npm run registry:build in js/react-shadcn) — pick whichever
distribution fits: dependency (npm) or owned code (registry).
React Renderer (js/react-shadcn)
@spawnflow-dx/react-shadcn renders complete forms from the schema contract — shadcn-styled widgets, react-hook-form + Zod under the hood:
- Widgets picked from descriptors (enum → select, relation → async searchable combobox fed by the options endpoint,
confirmedrules pair a confirmation input automatically) - Client-side validation compiled at runtime from the structured rules;
serverOnlyrules render a "server-checked" hint and map 422 errors back to fields - Context-aware: non-editable fields render disabled — the same component shows a different form per resolved permission variant
- Headless-friendly: override any widget via the registry (
widgets={{ combobox: MyCombobox }})
Live demo
Four forms — registration, change password, edit profile, billing details — plus a persona switcher demonstrating one component rendering three different billing forms from owner:active / owner:past_due / viewer contexts. Backed by a mock client serving contract-v1 JSON; swap in createHttpClient to point at a real API.
Escape Hatches
Use the chain for auth and ownership, then break out for custom logic:
Available accessors
Custom gates
Post-operation hooks
The Last Mile
Spawnflow handles ~80-85% of typical API operations. The remaining 15-20% — the "last mile" — is where generic CRUD ends and custom logic begins.
What Spawnflow absorbs
Operations that seem custom but decompose into CRUD with smart validation:
- State transitions (schedule, publish, archive) — a PATCH that sets
status. The context enum enforces which transitions are valid. - Deep clones (duplicate a campaign) — the frontend orchestrates a sequence of generic POST calls. No custom endpoint needed.
- Multi-step creation (create resource + related records) — the frontend coordinates multiple Spawnflow calls in sequence.
What stays as custom endpoints
| Category | Why | Chain still helps? |
|---|---|---|
| Aggregation / analytics | GROUP BY, date bucketing, cross-table joins | Yes — spawn → auth → resolve → ask for identity + ownership, then break out |
| External service calls | Spotify lookups, payment processing, S3 signed URLs | Yes — spawn → auth for identity context |
| Webhook receivers | No authenticated user, no subject | No — these are fire-and-forget event handlers |
| File / binary operations | Uploads, zip streams, CSV exports | No — response isn't a model |
Even for custom endpoints, the chain's escape hatches (getUser(), getInstance(), etc.) let you reuse auth and ownership without reimplementing them.
MCP Server
The contract is queryable and operable by AI agents over the Model Context Protocol. A thin adapter — every tool delegates to an existing owner (registry, serializer, eligibility, the Flow chain, artisan commands):
Dev tools (introspect schemas, evaluate eligibility verdicts, scaffold
resources from real tables, regenerate types) register only in the local
environment over stdio. Runtime CRUD tools (opt-in mcp.web, behind
auth:api) run the full Flow chain — ownership, contexts, eligibility and
wire coercion enforced exactly as over HTTP, returning the persisted record.
See docs/mcp.md.
Configuration Reference
Testing
Run the package tests:
The test suite uses Orchestra Testbench with an in-memory SQLite database. All fixtures are self-contained — no application models required. DB-introspection tests (--group=mysql-introspection) run against a real MySQL service in CI and skip locally without one.
JS side (js/): npm test runs the eligibility conformance suite (vitest) against the same resources/conformance/eligibility-fixtures.json the Pest suite uses, plus typecheck and demo build.
Roadmap
See docs/roadmap.md — shipped, in flight, and what stays demand-gated (with the exact triggers that unpark each item).
License
MIT. See LICENSE.
All versions of spawnflow-laravel with dependencies
illuminate/database Version ^11.0|^12.0|^13.0
illuminate/http Version ^11.0|^12.0|^13.0
illuminate/support Version ^11.0|^12.0|^13.0
illuminate/validation Version ^11.0|^12.0|^13.0