aurorawebsoftware/arflow
ArFlow is a Laravel/PHP package from Aurora Web Software for defining and managing workflows and approval flows. It helps model states, transitions, and process steps around Eloquent records so applications can automate business processes with a lightweight, configurable workflow engine.
Install and publish
composer require aurorawebsoftware/arflow
php artisan vendor:publish --tag="arflow-config" # check the tag in the README first
php artisan vendor:publish --tag="arflow-migrations"
php artisan migrate
Confirm the exact publish tags/namespaces by reading vendor/aurorawebsoftware/arflow/README.md — small packages like this one often ship the config and migrations under a single combined tag.
Look here first, in this order
config/arflow.php — the state column name, transition table/column names, and whether definitions are cached.src/ directory: find the trait you add to models (commonly HasWorkflow / InteractsWithWorkflow), the base Transition class, and the Events/ folder.state column on your model vs. a separate history/audit table). That distinction drives most of your design decisions later.Wire up your first model
use AuroraWebSoftware\ArFlow\Concerns\HasWorkflow; // verify actual namespace
class Order extends Model
{
use HasWorkflow;
}
Add the workflow state column to the model's table (usually a string column, e.g. workflow_state) and give it a sensible default in the migration.
First use case: an approval flow. Model draft → submitted → approved | rejected. This is the sweet spot: a handful of states, human-driven transitions, and side effects (notifications, timestamps) that map cleanly onto events. Get one model working end-to-end before adding a second, because workflow config tends to be global and shared.
Define transitions declaratively, not in controllers. Keep the state graph in one place per model (a workflows() method, a config array, or a dedicated definition class, depending on what the package exposes) so the graph is auditable and testable without hitting HTTP.
Guard transitions with policies. Rather than checking $user->isAdmin() inside a controller, put authorization in a Laravel policy and check canTransitionTo() (or equivalent) in a FormRequest or a small TransitionRequest. This keeps the workflow reusable across web, API, and queue contexts.
Treat side effects as listeners, not inline code.
Event::listen(WorkflowTransitioned::class, function ($event) {
if ($event->to === 'approved') {
$event->model->update(['approved_at' => now()]);
}
});
This keeps transition definitions pure and lets you disable side effects in tests.
Centralise the "attempt a transition" call. Put a single transitionTo() wrapper in a service or action class that wraps the call in a DB transaction, dispatches domain events after commit (DB::afterCommit or ShouldDispatchAfterCommit), and returns a result object instead of throwing raw exceptions. Every controller, command, and job then has one code path to reason about.
Expose available transitions to the UI. A method returning [['to' => 'approved', 'label' => 'Approve', 'allowed' => true], ...] per record lets you render buttons without duplicating the graph in Blade/Vue. Cache the definitions (static) but never the per-record allowed list.
Bulk/background transitions. For imports or scheduled jobs that move many records, chunk the query and dispatch one job per transition rather than looping inside a single transaction — you get retries and per-record audit trails for free.
Audit by default. If the package ships a history table, always render from it rather than from the current state column; the "who moved this to approved and when" question arrives within a week of launch.
status, the package's default state column may shadow or conflict with it. Decide early whether status is your workflow state or a separate business field, and rename one. Mixing them is the most common source of "why is my state wrong" bugs.php artisan migrate first and publishing afterwards leaves you editing migration files by hand. Publish, read them, then migrate.if ($model->state === 'submitted') guard. Wrap the transition in DB::transaction() with lockForUpdate() on the row, or rely on an optimistic-locking/version column if the package supports it. Don't assume the package handles this for you — check the source.ShouldDispatchAfterCommit / DB::afterCommit on listeners that have external effects.save() or refresh(). Call $model->refresh() before rendering or re-checking the state to avoid returning a stale value from an API resource.php artisan optimize:clear or a package-specific cache command) and document it.=== against strings — passing an enum where a string is expected can silently fail every guard.->inWorkflowState('approved')) so feature tests don't have to walk the whole graph to set up fixtures.before/after hook on the transition object, and a replaceable event class. Subclassing those is almost always cleaner than forking the package — pin the version and keep your customisations in app/.src/ directly, pin an exact version in composer.json, and keep a thin internal wrapper (your own service class) around every package call so a future swap only touches one file.How can I help you explore Laravel packages today?