Host Reference Patterns
Optional reading: This page describes reusable integration patterns for Laravel hosts that embed DBFlow into an existing ERP or admin product. It is not required to get started. Complete Host Integration and Filament UI first.
Alpha notice: Patterns below reflect lessons from real host pilots (including enterprise ERP integrations). Class names are illustrative — adapt naming to your application.
The hosted demo platform is the canonical tutorial environment. This page complements it when you need:
- Product-level feature flags and pilot rollouts
- Coexistence with a legacy approval engine
- Code-first definitions and the Standard Filament definition editor
- Host RBAC mapped to
PermissionChecker - Business guards on confirm / post / fulfill actions
When to use these patterns
| Situation | Pattern to adopt |
|---|---|
| DBFlow is optional or rolled out per module | Feature gate |
| Definitions live in PHP providers | Service provider boot chain |
| Operators see an empty definition editor after sync | Code workflow draft seeder |
| Ops needs a repeatable deploy step | Artisan sync command |
| Filament must respect host permissions | Permission checker |
| Definition editor shows permission-style assignees | Assignee options resolver |
confirm() must wait for workflow completion |
Business guard bridge |
| Submit starts workflow from an existing resource | Resource action bridge |
Recommended module layout
Keep DBFlow wiring out of generic controllers and fat Filament resources:
app/
├── Providers/
│ └── AppDbflowServiceProvider.php # register providers, resolvers, hooks; sync; seed drafts
├── Console/Commands/
│ └── SyncAppDbflowWorkflowsCommand.php # deploy / ops entrypoint
├── Services/Dbflow/
│ ├── CodeWorkflowDraftService.php # optional: seed drafts after code sync
│ └── PurchaseOrderWorkflowService.php # start / cancel helpers per domain
└── Support/Dbflow/
├── AppDbflowPilot.php # single feature-gate helper
├── AppDbflowWorkflowKeys.php # workflow + resolver key constants
├── Definitions/
│ └── PurchaseOrderApprovalWorkflowDefinitionProvider.php
├── AssigneeResolvers/
│ └── PurchaseOrderApproverAssigneeResolver.php
├── PurchaseOrderApprovalWorkflowHooks.php
└── Filament/
├── AppDbflowPermissionChecker.php
├── AppDbflowWorkflowableLabelResolver.php
└── AppDbflowPermissionAssigneeOptionsResolver.php
app/Support/PluginIntegration/ # optional boundary for legacy engine coexistence
├── DbflowFilamentBridge.php # Filament header actions on business resources
└── PurchaseOrderApprovalGuardBridge.php
Filament panel registration stays in PanelProvider — gate DBFlowFilamentPanel::register($panel) with the same pilot helper.
Boot order
Host app boots
│
├─► AppDbflowServiceProvider (if pilot / feature enabled)
│ register WorkflowDefinitionProvider(s)
│ register AssigneeResolver key(s)
│ register WorkflowHooks
│ SyncWorkflowDefinitions::handle()
│ CodeWorkflowDraftService::seedMissingDrafts() [optional]
│
├─► AdminPanelProvider
│ DBFlowFilamentPanel::register($panel) [if pilot enabled]
│
└─► Business request
Resource action → DBFlow::start()
My Tasks → DBFlow::approve() / reject()
confirm() → GuardBridge checks terminal workflow state
Core does not auto-run sync or draft seeding. The host owns when those actions run (boot, deploy hook, or Artisan).
Feature gate
Wrap all DBFlow surfaces — service provider boot, Filament panel registration, resource actions, and business guards — behind one helper:
final class AppDbflowPilot
{
public static function isEnabled(): bool
{
return (bool) config('app.dbflow.enabled', false)
&& (bool) config('dbflow.enabled', true)
&& (bool) config('dbflow-filament.enabled', true);
}
public static function purchaseOrder(): bool
{
return self::isEnabled()
&& (bool) config('app.dbflow.purchase_order_pilot', false);
}
}
DBFlowFilamentPanel reads only dbflow-filament.enabled. A host-level gate prevents half-enabled stacks where UI is visible but definitions were never registered.
Use per-module flags (purchase_order_pilot) so one document type can pilot DBFlow while others keep a legacy engine.
Service provider boot chain
Centralize Core registration in a dedicated provider; early-return when the pilot is off:
public function boot(): void
{
if (! AppDbflowPilot::isEnabled()) {
return;
}
DBFlow::registerDefinitionProvider($registry, new PurchaseOrderApprovalWorkflowDefinitionProvider());
DBFlow::registerAssigneeResolver($registry, 'purchase_order_approver', $resolver);
DBFlow::registerWorkflowHooks($registry, 'purchase_order_approval', PurchaseOrderApprovalWorkflowHooks::class);
if (config('app.dbflow.auto_sync_definitions', true)) {
app(SyncWorkflowDefinitions::class)->handle();
}
if (config('app.dbflow.seed_code_workflow_drafts', true)) {
app(CodeWorkflowDraftService::class)->seedMissingDraftsForCodeWorkflows();
}
}
Register the provider in bootstrap/providers.php.
Code workflow draft seeder
After SyncWorkflowDefinitions, code-owned workflows are published but often have no draft. The Standard WorkflowResource editor requires Workflow::hasDraft().
Extract seeding into a small service that iterates registered providers:
final class CodeWorkflowDraftService
{
public function seedMissingDraftsForCodeWorkflows(): array
{
$seeded = [];
foreach ($this->definitionRegistry->providers() as $provider) {
$workflow = Workflow::query()->where('key', $provider->key())->first();
if (! $workflow || $this->isUiOwned($workflow)) {
continue;
}
if ($workflow->hasDraft() || ! $workflow->hasPublishedVersion()) {
continue;
}
$definition = $workflow->currentDefinition();
// merge key, name, description from workflow row …
$this->saveWorkflowDraft->handle($workflow, $definition);
$seeded[] = $workflow->key;
}
return $seeded;
}
}
Rules:
- Skip
source = uiworkflows — do not overwrite UI-authored drafts. - Skip workflows that already have a draft.
- Run after every sync in boot, deploy, or your Artisan command.
See also Filament UI → Code-synced workflows.
Artisan sync command
Wrap SyncWorkflowDefinitions for operators and CI:
final class SyncAppDbflowWorkflowsCommand extends Command
{
protected $signature = 'app:dbflow-sync-workflows';
public function handle(SyncWorkflowDefinitions $sync, CodeWorkflowDraftService $drafts): int
{
if (! AppDbflowPilot::isEnabled()) {
$this->warn('DBFlow pilot disabled — skipped.');
return self::SUCCESS;
}
$summary = $sync->handle();
// print created / updated / unchanged keys …
foreach ($drafts->seedMissingDraftsForCodeWorkflows() as $key) {
$this->line("Draft seeded: {$key}");
}
return self::SUCCESS;
}
}
Core ships no bundled sync command — this is host responsibility.
Permission checker
Replace AllowAllPermissionChecker before any shared environment. Map dbflow.* abilities to your existing RBAC:
final class AppDbflowPermissionChecker implements PermissionChecker
{
public function can(mixed $user, string $ability, mixed $record = null): bool
{
return match ($ability) {
'dbflow.tasks.view',
'dbflow.tasks.approve',
'dbflow.tasks.reject' => $this->permissions->allows($user, 'documents.approve'),
'dbflow.workflow_instances.view',
'dbflow.workflow_instances.view_any' => $this->permissions->allows($user, 'documents.view'),
'dbflow.definitions.view',
'dbflow.definitions.update',
'dbflow.definitions.publish' => $this->permissions->allows($user, 'system.settings'),
default => false,
};
}
}
Bind via permission_checker_class in config/dbflow-filament.php. Ability names are listed in Permissions.
Assignee options resolver
For assignees.type: permission in definitions, Core resolves user IDs through DBFlow::registerAssigneeResolver($key, …). Filament needs a separate UI contract so authors see friendly labels:
| Layer | Key example | Class |
|---|---|---|
| Core runtime | purchase_order_approver |
PurchaseOrderApproverAssigneeResolver |
| Filament editor | same string | AppDbflowPermissionAssigneeOptionsResolver |
The JSON value must match the registry key — it is not your Spatie permission name.
Business guard bridge
Core does not block confirm(), payment capture, or inventory posting. Add a thin bridge your domain service calls before mutating state:
final class PurchaseOrderApprovalGuardBridge
{
public static function canConfirm(PurchaseOrder $order): bool
{
if (! AppDbflowPilot::purchaseOrder()) {
return true; // legacy path or DBFlow off
}
if ($order->status !== 'draft') {
return true;
}
$instance = $order->latestWorkflowInstance('purchase_order_approval');
return $instance?->status === WorkflowInstanceStatus::Approved;
}
public static function assertCanConfirm(PurchaseOrder $order): void
{
if (! self::canConfirm($order)) {
throw ValidationException::withMessages([
'status' => 'Complete workflow approval before confirming.',
]);
}
}
}
Call assertCanConfirm() from PurchaseOrderService::confirm() (or equivalent). Gate Filament actions with the same rules.
Resource action bridge
Keep Filament resource classes thin. A bridge class registers submit actions and hides legacy approval UI when the pilot is active:
// In EditPurchaseOrder::getHeaderActions()
if (AppDbflowPilot::purchaseOrder()) {
return DbflowFilamentBridge::headerActions($this);
}
return LegacyApprovalBridge::headerActions($this);
Submit actions call DBFlow::start($workflowKey, $record, $user) and guard visibility with hasRunningWorkflow($key). See Filament Resource Actions.
Implement WorkflowRouteResolvable::getWorkflowShowUrl() on the model so My Tasks and instance pages link back to your resource.
Case study: purchase order pilot
An ERP host integrated DBFlow for purchase order approval during alpha:
| Piece | Responsibility |
|---|---|
AppDbflowPilot |
DBFLOW_ENABLED + module pilot flag + Filament toggle |
PurchaseOrderApprovalWorkflowDefinitionProvider |
Single approval node, permission assignee → resolver key |
PurchaseOrderApproverAssigneeResolver |
Returns user IDs with document edit permission |
PurchaseOrderApprovalWorkflowHooks |
Maps approved / rejected / cancelled to order status columns |
CodeWorkflowDraftService |
Fills Filament editor after code sync |
AppDbflowPermissionChecker |
Maps dbflow.* to ERP permission constants |
PurchaseOrderApprovalGuardBridge |
Blocks confirm() until workflow reaches approved |
DbflowFilamentBridge |
Submit-for-approval on edit page; mutually exclusive with legacy light approval |
This is one module in a larger product — copy the shape, not every class name.
What not to copy blindly
- Dual engines without mutual exclusion — hide legacy submit/approve UI when DBFlow pilot is on.
- Publishing from Filament on code-owned workflows — document who owns the definition (code vs UI) before operators publish.
- Skipping
permission_checker_class— demo-friendly defaults are unsafe in production. - Guarding only in Filament — enforce confirm/post rules in domain services too.
- Product licensing, plugin marketplaces, or install wizards — out of scope for DBFlow docs.
Checklist (host product)
- Feature gate covers provider boot, panel registration, and bridges.
- Workflow keys and resolver keys live in one constants class.
- Sync runs on deploy; optional draft seeder runs after sync.
permission_checker_classandpermission_assignee_options_resolver_classare bound.- Resource submit →
DBFlow::start(); approve/reject → My Tasks (or equivalent with actor). - Downstream mutations use a guard bridge + hooks for status columns.
- Feature tests cover start, approve, guard failure, and pilot off fallback.
What's next
- Host Integration — required Core checklist
- Filament UI — Standard package registration and troubleshooting
- Filament Resource Actions — start workflows from resources
- Testing Workflows — PHPUnit patterns
- Refund Approval — tutorial walkthrough from the hosted demo