FlowPickerBuilder
The FlowPickerBuilder defines the constraints and selection strategy used by the FlowPicker to choose a Flow. It centralizes all filters, ordering preferences, match strategy, fallback cascade steps, and performance hints.
Namespace
JobMetric\Flow\Support\FlowPickerBuilder
Where and When to Use FlowPickerBuilder
Primary Use Cases
FlowPickerBuilder is used in the following scenarios:
-
HasWorkflow Trait Integration
- When models use
HasWorkflowtrait, they overridebuildFlowPicker()method - This method receives a
FlowPickerBuilderinstance to configure flow selection - Used automatically during model creation to select the appropriate flow
- When models use
-
HasFlow Trait Integration
- When models use
HasFlowtrait, they also usebuildFlowPicker()internally - Uses
forceFlowIdResolverto bypass complex selection logic - Directly binds flows by ID while still using the builder pattern
- When models use
-
Flow Service Preview
- When using
Flow::previewPick()to preview which flow would be selected - Allows testing flow selection logic without actually binding flows
- Useful for debugging and validation
- When using
-
Manual Flow Selection
- When manually selecting flows outside of automatic binding
- When building custom flow selection logic
- When implementing advanced flow routing scenarios
Key Scenarios
Scenario 1: Multi-Tenant Applications
When: Different tenants need different workflows for the same model type.
How: Use subjectScope() to partition flows by tenant:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->subjectScope((string)$this->tenant_id);
}
Scenario 2: Environment-Specific Workflows
When: Different workflows for production, staging, and development.
How: Use environment() filter:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->environment(config('app.env'));
}
Scenario 3: Channel-Based Workflows
When: Different workflows for web, API, and mobile channels.
How: Use channel() filter:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->channel(request()->header('X-Channel', 'web'));
}
Scenario 4: A/B Testing and Gradual Rollouts
When: Gradually rolling out new workflows to a percentage of users.
How: Use rollout configuration:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->evaluateRollout(true)
->rolloutNamespace('order_v2')
->rolloutKeyResolver(function ($model) {
return (string)$model->user_id;
});
}
Scenario 5: Collection-Based Workflows
When: Different workflows based on model attributes (e.g., premium vs standard orders).
How: Use subjectCollection():
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->subjectCollection($this->order_type);
}
Scenario 6: Scheduled Workflow Activation
When: Workflows that activate/deactivate at specific times.
How: Use active window constraints:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'));
}
Scenario 7: Version Management
When: Managing multiple versions of workflows and migrating between them.
How: Use version constraints:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->versionAtLeast(1)
->versionAtMost(3);
}
Scenario 8: Fallback Strategies
When: Need graceful degradation when exact flow match is not found.
How: Use fallback cascade:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
]);
}
When NOT to Use FlowPickerBuilder
You typically don't need to use FlowPickerBuilder directly when:
- Using
HasFlowtrait with simple flow ID resolution (it's handled internally) - Working with single, fixed flows per model type
- Flow selection logic is straightforward and doesn't change
In these cases, use HasFlow instead of HasWorkflow.
Overview
The builder provides a fluent interface to configure:
- Subject filters: Type, scope, collection
- Environment/Channel filters: Production, staging, web, API, etc.
- Active constraints: Status and time windows
- Rollout gating: Gradual feature rollouts
- Version constraints: Exact version or version ranges
- Include/Exclude lists: Whitelist and blacklist flows
- Preferential ordering: Boost certain flows in ordering
- Match strategy: BEST (default) or FIRST
- Fallback cascade: Progressive constraint relaxation
- Request caching: Performance optimization
Constants
Match Strategy
FlowPickerBuilder::STRATEGY_BEST // Best candidate based on ordering (default)
FlowPickerBuilder::STRATEGY_FIRST // First matching record
Fallback Steps
FlowPickerBuilder::FB_DROP_CHANNEL // Remove channel filter
FlowPickerBuilder::FB_DROP_ENVIRONMENT // Remove environment filter
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW // Ignore active window
FlowPickerBuilder::FB_DISABLE_ROLLOUT // Disable rollout checks
FlowPickerBuilder::FB_DROP_REQUIRE_DEFAULT // Don't require is_default
Subject Configuration
subjectType()
Set the model class that flows are being selected for. This is the primary filter and is required for flow selection.
$builder->subjectType(Order::class);
Parameters:
class-string<Model> $class: Fully qualified model class name
Returns: $this (fluent interface)
Behavior:
- Filters flows where
subject_typecolumn matches the provided class - Must be set before flow selection can work
- Used internally by
HasWorkflowandHasFlowtraits
Example:
$builder->subjectType(\App\Models\Order::class);
// Matches flows with subject_type = 'App\Models\Order'
subjectScope()
Set optional subject scope to partition flows by domain (tenant, organization, etc.). This allows multiple flows for the same model type based on scope.
$builder->subjectScope((string)$order->tenant_id);
$builder->subjectScope(null); // Clear scope filter
Parameters:
?string $scope: Scope identifier (e.g., tenant ID, org ID) or null to clear
Returns: $this (fluent interface)
Behavior:
- Filters flows where
subject_scopecolumn matches the provided value - When
null, no scope filtering is applied - Useful for multi-tenant applications
Example:
// Tenant-specific flows
$builder->subjectScope((string)$order->tenant_id);
// Matches flows with subject_scope = '123' (tenant ID)
// Clear scope to match any scope
$builder->subjectScope(null);
Use Cases:
- Multi-tenant SaaS applications
- Organization-based workflows
- Department-specific processes
subjectCollection()
Set optional subject collection to further partition flows. Collections allow different workflows for the same model type based on attributes.
$builder->subjectCollection($order->collection);
$builder->subjectCollection(null); // Clear collection filter
Parameters:
?string $collection: Collection identifier (e.g., 'premium', 'standard', 'enterprise') or null to clear
Returns: $this (fluent interface)
Behavior:
- Filters flows where
subject_collectioncolumn matches the provided value - When
null, no collection filtering is applied - Can be combined with
subjectScope()for fine-grained control
Example:
// Premium order workflow
$builder->subjectCollection('premium');
// Matches flows with subject_collection = 'premium'
// Standard order workflow
$builder->subjectCollection('standard');
// Matches flows with subject_collection = 'standard'
Use Cases:
- Different workflows for premium vs standard customers
- Collection-based business rules
- Attribute-driven workflow selection
Environment and Channel
environment()
Filter flows by environment. This restricts flows to a specific deployment environment.
$builder->environment('production');
$builder->environment(config('app.env'));
$builder->environment(null); // Clear filter
Parameters:
?string $environment: Environment name (e.g., 'production', 'staging', 'development') or null to clear
Returns: $this (fluent interface)
Behavior:
- Filters flows where
environmentcolumn matches the provided value - When
null, no environment filtering is applied - Common values: 'production', 'staging', 'development', 'testing'
Example:
// Production-only flows
$builder->environment('production');
// Environment from config
$builder->environment(config('app.env'));
// Clear to match any environment
$builder->environment(null);
Use Cases:
- Different workflows per deployment environment
- Testing workflows in staging
- Production-specific business rules
channel()
Filter flows by channel. This restricts flows based on the request channel (web, API, mobile, etc.).
$builder->channel('web');
$builder->channel(request()->header('X-Channel', 'web'));
$builder->channel(null); // Clear filter
Parameters:
?string $channel: Channel name (e.g., 'web', 'api', 'mobile', 'admin') or null to clear
Returns: $this (fluent interface)
Behavior:
- Filters flows where
channelcolumn matches the provided value - When
null, no channel filtering is applied - Can be determined from request headers, routes, or user context
Example:
// Web-only flows
$builder->channel('web');
// API-only flows
$builder->channel('api');
// From request header
$builder->channel(request()->header('X-Channel', 'web'));
// From route
$builder->channel(request()->is('api/*') ? 'api' : 'web');
Use Cases:
- Different workflows for web vs API requests
- Mobile-specific workflows
- Admin panel workflows
preferEnvironments()
Set preferred environments for ordering (not filtering). This boosts certain environments in the ordering without filtering them out.
$builder->preferEnvironments(['production', 'staging', 'development']);
Parameters:
array<int,string> $envs: Array of preferred environment names
Returns: $this (fluent interface)
Behavior:
- Not a filter - doesn't exclude other environments
- Earlier items in the array rank higher in ordering
- Uses CASE-based SQL ordering to boost preferred values
- Empty strings and null values are filtered out
- Values are converted to strings
Example:
// Prefer production, then staging, then development
$builder->preferEnvironments(['production', 'staging', 'development']);
// If multiple flows match, production will be selected first
// But development flows are still eligible if production doesn't match
Use Cases:
- Prefer production flows but allow staging as fallback
- Environment priority ordering
- Graceful degradation
preferChannels()
Set preferred channels for ordering (not filtering). This boosts certain channels in the ordering without filtering them out.
$builder->preferChannels(['web', 'api', 'mobile']);
Parameters:
array<int,string> $channels: Array of preferred channel names
Returns: $this (fluent interface)
Behavior:
- Not a filter - doesn't exclude other channels
- Earlier items in the array rank higher in ordering
- Uses CASE-based SQL ordering to boost preferred values
- Empty strings and null values are filtered out
- Values are converted to strings
Example:
// Prefer web, then API, then mobile
$builder->preferChannels(['web', 'api', 'mobile']);
Use Cases:
- Prefer web flows but allow API as fallback
- Channel priority ordering
- Multi-channel support with preferences
Active Constraints
onlyActive()
Require active flows. When enabled, only flows with status=true are eligible, and optionally time window checks are applied.
$builder->onlyActive(true); // Enforce active constraints
$builder->onlyActive(false); // Disable active checks
Parameters:
bool $onlyActive: Whether to enforce active constraints
Returns: $this (fluent interface)
Default: true
Behavior:
- When
true: Requiresstatus = trueand optionally checksactive_from/active_totime windows - When
false: No active status or time window checks are applied - Time window checks can be disabled separately with
ignoreTimeWindow(true)
Example:
// Only active flows
$builder->onlyActive(true);
// Include inactive flows (for testing or admin)
$builder->onlyActive(false);
Use Cases:
- Production workflows (only active)
- Testing scenarios (include inactive)
- Admin interfaces (show all flows)
ignoreTimeWindow()
Ignore active_from/active_to time window checks while keeping status=true requirement (if onlyActive is true).
$builder->ignoreTimeWindow(true); // Ignore time window
$builder->ignoreTimeWindow(false); // Enforce time window
Parameters:
bool $ignore = true: Whether to ignore time window checks
Returns: $this (fluent interface)
Default: false
Behavior:
- When
true: Only checksstatus = true, ignoresactive_fromandactive_to - When
false: Checks bothstatus = trueand time window constraints - Only has effect when
onlyActive(true)is set - Useful for testing or when time windows are not needed
Example:
// Check status only, ignore time windows
$builder->onlyActive(true)
->ignoreTimeWindow(true);
// Check both status and time windows
$builder->onlyActive(true)
->ignoreTimeWindow(false);
Use Cases:
- Testing without time constraints
- Workflows without scheduled activation
- Simplified active checks
timeNow()
Set the reference "now" time in UTC for deterministic time window evaluation. This allows testing with specific times and ensures consistent evaluation.
$builder->timeNow(Carbon::now('UTC'));
Parameters:
DateTimeInterface $now: UTC instant for time comparisons
Returns: $this (fluent interface)
Behavior:
- Sets the reference time used to evaluate
active_fromandactive_toconstraints - Must be in UTC timezone
- When not set,
Carbon::now('UTC')is used by FlowPicker - Useful for testing and deterministic evaluation
Example:
// Use current time
$builder->timeNow(Carbon::now('UTC'));
// Use specific time for testing
$builder->timeNow(Carbon::parse('2024-01-01 12:00:00', 'UTC'));
// Use time from model
$builder->timeNow($order->created_at->utc());
Use Cases:
- Testing with specific times
- Deterministic evaluation in tests
- Time-based flow activation/deactivation
- Scheduled workflow scenarios
Rollout Configuration
evaluateRollout()
Enable or disable rollout gating. Rollout allows gradual feature deployment by assigning flows to a percentage of users based on a stable key.
$builder->evaluateRollout(true); // Enable rollout
$builder->evaluateRollout(false); // Disable rollout
Parameters:
bool $enabled: Whether rollout should be applied
Returns: $this (fluent interface)
Default: true
Behavior:
- When
true: Applies rollout percentage checks using stable bucket hashing - When
false: Ignoresrollout_pctcolumn, all flows are eligible - Requires
rolloutKeyResolverto be set for proper operation - If no rollout key is available, only flows with
rollout_pct IS NULLare eligible
Example:
// Enable gradual rollout
$builder->evaluateRollout(true);
// Disable for testing
$builder->evaluateRollout(false);
Use Cases:
- A/B testing new workflows
- Gradual feature rollouts
- Canary deployments
- Percentage-based user segmentation
rolloutNamespace()
Set namespace to isolate rollout bucket spaces across different domains or features. This ensures that rollout percentages are independent for different features.
$builder->rolloutNamespace('order_v2');
Parameters:
?string $ns: Namespace string or null to clear
Returns: $this (fluent interface)
Behavior:
- Used in hash calculation to isolate buckets
- Different namespaces create independent bucket spaces
- When
null, no namespace is used in hashing - Combined with salt and key to compute stable bucket
Example:
// Isolate order workflow rollout
$builder->rolloutNamespace('order_v2');
// Isolate payment workflow rollout (independent)
$builder->rolloutNamespace('payment_v3');
Use Cases:
- Multiple feature rollouts running simultaneously
- Independent A/B tests
- Domain-specific rollouts
rolloutSalt()
Set salt to further stabilize or segregate rollout hashing. Salt allows version-specific or context-specific bucket assignment.
$builder->rolloutSalt('2024-01');
Parameters:
?string $salt: Salt string or null to clear
Returns: $this (fluent interface)
Behavior:
- Used in hash calculation for additional stability/segregation
- Changing salt redistributes buckets (users get different flows)
- When
null, no salt is used in hashing - Combined with namespace and key to compute stable bucket
Example:
// Version-specific salt
$builder->rolloutSalt('2024-01');
// Date-based salt for monthly redistribution
$builder->rolloutSalt(now()->format('Y-m'));
// Clear salt
$builder->rolloutSalt(null);
Use Cases:
- Version-specific rollouts
- Time-based bucket redistribution
- Further segregation of rollout spaces
rolloutKeyResolver()
Set resolver callback that returns a stable rollout key per model instance. The same key will always fall into the same bucket, ensuring consistent assignment.
$builder->rolloutKeyResolver(function (Model $model): ?string {
return (string)$model->user_id;
});
Parameters:
callable(Model):(?string) $resolver: Callback that returns a stable key or null
Returns: $this (fluent interface)
Behavior:
- Called with the model instance to get a stable key
- Key is used to compute bucket (0-99) via hash
- Same key always produces same bucket (stable assignment)
- When
nullor empty string, only flows withrollout_pct IS NULLare eligible - Common keys:
user_id,order_id,customer_id
Example:
// Use user ID for consistent assignment
$builder->rolloutKeyResolver(function ($model) {
return (string)$model->user_id;
});
// Use order ID
$builder->rolloutKeyResolver(function ($model) {
return (string)$model->getKey();
});
// Use combination
$builder->rolloutKeyResolver(function ($model) {
return $model->user_id . '_' . $model->tenant_id;
});
Bucket Calculation:
bucket = crc32(namespace + '|' + salt + '|' + key) % 100
Use Cases:
- Consistent user assignment (same user always gets same flow)
- Order-based assignment
- Customer-based segmentation
Version Constraints
versionEquals()
Pin selection to an exact version. When set, versionMin and versionMax are ignored.
$builder->versionEquals(2); // Only version 2
Parameters:
int $version: Exact version number
Returns: $this (fluent interface)
Behavior:
- Filters flows where
versioncolumn equals the provided value - Disables
versionMinandversionMaxif they were set - Most restrictive version constraint
- Useful for testing specific versions or pinning to a known good version
Example:
// Only version 2 flows
$builder->versionEquals(2);
// This will ignore versionMin/versionMax
$builder->versionEquals(2)
->versionAtLeast(1) // Ignored
->versionAtMost(3); // Ignored
Use Cases:
- Testing specific workflow versions
- Pinning to stable versions
- Version-specific deployments
versionAtLeast()
Set minimum allowed version (inclusive). Ignored when versionEquals is set.
$builder->versionAtLeast(1); // Version >= 1
Parameters:
int $min: Minimum version number (inclusive)
Returns: $this (fluent interface)
Behavior:
- Filters flows where
version >= $min - Ignored if
versionEqualsis set - Can be combined with
versionAtMostfor range - Useful for excluding older versions
Example:
// Versions 1 and above
$builder->versionAtLeast(1);
// Combined with maximum
$builder->versionAtLeast(1)
->versionAtMost(3); // Versions 1-3
Use Cases:
- Excluding deprecated versions
- Minimum version requirements
- Version migration scenarios
versionAtMost()
Set maximum allowed version (inclusive). Ignored when versionEquals is set.
$builder->versionAtMost(3); // Version <= 3
Parameters:
int $max: Maximum version number (inclusive)
Returns: $this (fluent interface)
Behavior:
- Filters flows where
version <= $max - Ignored if
versionEqualsis set - Can be combined with
versionAtLeastfor range - Useful for excluding newer versions
Example:
// Versions 3 and below
$builder->versionAtMost(3);
// Combined with minimum
$builder->versionAtLeast(1)
->versionAtMost(3); // Versions 1-3
Use Cases:
- Excluding beta versions
- Maximum version limits
- Gradual version migration
versionMin() / versionMax()
Alternative method names for versionAtLeast() and versionAtMost(). Functionally identical.
$builder->versionMin(1)->versionMax(3); // Versions 1-3
Parameters:
versionMin(int $min): Minimum version (same asversionAtLeast)versionMax(int $max): Maximum version (same asversionAtMost)
Returns: $this (fluent interface)
Note: These are aliases for versionAtLeast() and versionAtMost(). Use whichever naming you prefer.
Include/Exclude/Prefer
includeFlows()
Restrict selection to only these flow IDs (whitelist). Only flows with IDs in this list are eligible.
$builder->includeFlows([1, 2, 3]); // Only these flows
Parameters:
array<int,int> $ids: Array of allowed flow IDs
Returns: $this (fluent interface)
Behavior:
- Filters flows where
id IN ($ids) - Empty array means no whitelist (all flows eligible)
- Duplicate IDs are removed
- Non-integer values are converted to integers
- Array is re-indexed
Example:
// Only flows 1, 2, 3
$builder->includeFlows([1, 2, 3]);
// Single flow
$builder->includeFlows([1]);
// Clear whitelist
$builder->includeFlows([]);
Use Cases:
- Testing specific flows
- Limited flow selection
- Flow subset scenarios
excludeFlows()
Exclude these flow IDs from selection (blacklist). Flows with IDs in this list are not eligible.
$builder->excludeFlows([4, 5]); // Exclude these flows
Parameters:
array<int,int> $ids: Array of disallowed flow IDs
Returns: $this (fluent interface)
Behavior:
- Filters flows where
id NOT IN ($ids) - Empty array means no blacklist
- Duplicate IDs are removed
- Non-integer values are converted to integers
- Array is re-indexed
Example:
// Exclude flows 4, 5
$builder->excludeFlows([4, 5]);
// Exclude deprecated flows
$builder->excludeFlows([10, 11, 12]);
// Clear blacklist
$builder->excludeFlows([]);
Use Cases:
- Excluding deprecated flows
- Removing problematic flows
- Testing without specific flows
preferFlows()
Prefer these flow IDs in ordering (not filtering). This boosts certain flows in the ordering without excluding others.
$builder->preferFlows([1, 2]); // Boost these in ordering
Parameters:
array<int,int> $ids: Array of preferred flow IDs
Returns: $this (fluent interface)
Behavior:
- Not a filter - doesn't exclude other flows
- Earlier IDs in the array rank higher in ordering
- Uses CASE-based SQL ordering to boost preferred IDs
- Duplicate IDs are removed
- Non-integer values are converted to integers
- Array is re-indexed
Example:
// Prefer flows 1, 2, then 3
$builder->preferFlows([1, 2, 3]);
// If multiple flows match, flow 1 will be selected first
// But other flows are still eligible if 1, 2, 3 don't match
Use Cases:
- Preferring specific flows but allowing fallbacks
- Priority-based flow selection
- Testing flows with fallback options
Ordering
orderByDefault()
Apply the default ordering strategy. This is the standard ordering used when no custom ordering is specified.
$builder->orderByDefault();
Returns: $this (fluent interface)
Default Ordering (applied in order):
version DESC- Higher versions preferredis_default DESC- Default flows preferredordering DESC- Higher ordering value preferredid DESC- Higher ID as tiebreaker
Behavior:
- Sets the
orderingCallbackto default implementation - Used automatically if no custom
orderBy()is set - Only applies when
matchStrategyisSTRATEGY_BEST - When
STRATEGY_FIRST, minimal ordering is used instead
Example:
// Use default ordering
$builder->orderByDefault();
// This is equivalent to:
$builder->orderBy(function ($query) {
$query->orderByDesc('version')
->orderByDesc('is_default')
->orderByDesc('ordering')
->orderByDesc('id');
});
Use Cases:
- Standard flow selection
- Most common use case
- When no special ordering is needed
orderBy()
Provide a custom ORDER BY callback to override default ordering. This allows complete control over how flows are sorted.
$builder->orderBy(function (Builder $query): void {
$query->orderBy('priority', 'desc')
->orderBy('created_at', 'asc');
});
Parameters:
Closure(Builder):void $callback: Ordering callback that receives the query builder
Returns: $this (fluent interface)
Behavior:
- Replaces default ordering with custom callback
- Callback receives
Builder $queryparameter - Can apply any ordering logic
- Only used when
matchStrategyisSTRATEGY_BEST - When
STRATEGY_FIRST, this is ignored
Example:
// Custom priority-based ordering
$builder->orderBy(function ($query) {
$query->orderByDesc('priority')
->orderByAsc('created_at');
});
// Complex ordering with conditions
$builder->orderBy(function ($query) {
$query->orderByRaw('CASE WHEN is_default = 1 THEN 0 ELSE 1 END')
->orderByDesc('version');
});
Use Cases:
- Custom business logic ordering
- Priority-based selection
- Complex sorting requirements
matchStrategy()
Set the selection strategy for choosing flows when multiple candidates match.
$builder->matchStrategy(FlowPickerBuilder::STRATEGY_BEST); // Best candidate
$builder->matchStrategy(FlowPickerBuilder::STRATEGY_FIRST); // First match
Parameters:
string $strategy: Strategy constant (STRATEGY_BESTorSTRATEGY_FIRST)
Returns: $this (fluent interface)
Default: STRATEGY_BEST
STRATEGY_BEST:
- Returns the best candidate based on ordering rules
- Applies full ordering (version, is_default, ordering, id)
- Uses custom
orderBy()if provided - Most predictable and recommended
STRATEGY_FIRST:
- Returns the very first matching record
- Minimal ordering (just
id ASC) - Ignores custom
orderBy()callback - Faster but less predictable
- Useful for testing or when order doesn't matter
Example:
// Best candidate (default)
$builder->matchStrategy(FlowPickerBuilder::STRATEGY_BEST);
// First match (faster)
$builder->matchStrategy(FlowPickerBuilder::STRATEGY_FIRST);
Use Cases:
- STRATEGY_BEST: Production use, predictable selection
- STRATEGY_FIRST: Testing, performance-critical scenarios
pickFirstMatch()
Convenience method to switch between first match and best match strategies.
$builder->pickFirstMatch(true); // STRATEGY_FIRST
$builder->pickFirstMatch(false); // STRATEGY_BEST
Parameters:
bool $enabled = true: Whether to pick first match
Returns: $this (fluent interface)
Behavior:
- When
true: Sets strategy toSTRATEGY_FIRST - When
false: Sets strategy toSTRATEGY_BEST - Convenience wrapper around
matchStrategy()
Example:
// Use first match
$builder->pickFirstMatch(true);
// Use best match
$builder->pickFirstMatch(false);
Custom Filters
where()
Attach a custom WHERE callback to extend query constraints. This allows adding any custom filtering logic beyond the standard filters.
$builder->where(function (Builder $query, Model $model): void {
$query->where('custom_field', $model->some_attribute);
});
Parameters:
Closure(Builder,Model):void $callback: Callback receiving the query builder and model instance
Returns: $this (fluent interface)
Behavior:
- Callback is called after all standard filters are applied
- Receives both
Builder $queryandModel $modelparameters - Can add any WHERE conditions
- Multiple callbacks can be chained
- Disables request caching (dynamic callbacks prevent safe caching)
Example:
// Single custom filter
$builder->where(function ($query, $model) {
$query->where('custom_field', $model->some_attribute);
});
// Multiple filters
$builder->where(function ($query, $model) {
$query->where('field1', $model->attr1);
})
->where(function ($query, $model) {
$query->where('field2', $model->attr2);
});
// Complex conditions
$builder->where(function ($query, $model) {
if ($model->is_premium) {
$query->where('is_premium_flow', true);
} else {
$query->where('is_premium_flow', false);
}
});
Use Cases:
- Custom business logic filtering
- Complex conditional filters
- Model-specific requirements
- Dynamic filtering based on model state
Important: Using where() disables request caching because callbacks are dynamic and may depend on model state.
Fallback Cascade
fallbackCascade()
Define an ordered list of fallback steps to progressively relax constraints when no flow matches initially. Steps are tried in order until a match is found.
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
]);
Parameters:
array<int,string> $steps: Ordered array of fallback step constants
Returns: $this (fluent interface)
Behavior:
- Steps are tried in order until a match is found
- First step that produces candidates wins (selection stops)
- Invalid step constants are automatically filtered out
- Only recognized
FB_*constants are kept - Empty array means no fallback (returns null if no match)
Available Fallback Steps:
-
FB_DROP_CHANNEL: Remove the channel filter- Allows flows from any channel
- Useful when channel-specific flows aren't available
-
FB_DROP_ENVIRONMENT: Remove the environment filter- Allows flows from any environment
- Useful when environment-specific flows aren't available
-
FB_IGNORE_TIMEWINDOW: Ignoreactive_from/active_tochecks- Keeps
status=truerequirement - Useful when flows are outside active window
- Keeps
-
FB_DISABLE_ROLLOUT: Disable rollout gating entirely- Ignores
rollout_pctcolumn - Useful when rollout prevents matching
- Ignores
-
FB_DROP_REQUIRE_DEFAULT: Don't requireis_default=true- Allows non-default flows
- Useful when only default flows are required initially
Example:
// Progressive fallback
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL, // Try without channel
FlowPickerBuilder::FB_DROP_ENVIRONMENT, // Try without environment
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW, // Try ignoring time window
]);
// Invalid steps are ignored
$builder->fallbackCascade([
'invalid_step', // Ignored
FlowPickerBuilder::FB_DROP_CHANNEL, // Used
]);
Use Cases:
- Graceful degradation when exact match not found
- Flexible flow selection
- Fallback to less specific flows
- Ensuring a flow is always found when possible
Force Flow ID
forceFlowIdResolver()
Provide a resolver that can force a specific Flow ID from the model. When set, this bypasses all complex selection logic and directly resolves the Flow by ID.
$builder->forceFlowIdResolver(function (Model $model): ?int {
return $model->flowId(); // Direct Flow ID
});
Parameters:
callable(Model):(?int) $resolver: Callback returning a Flow ID or null
Returns: $this (fluent interface)
Behavior:
- Bypasses all selection logic (subject, environment, channel, rollout, etc.)
- Directly loads Flow by ID from database
- Still validates active constraints if
onlyActive(true)is set - When resolver returns
null, normal selection logic is used - Disables request caching (dynamic resolver prevents safe caching)
Used by: HasFlow trait for direct ID binding
Example:
// Direct Flow ID from model
$builder->forceFlowIdResolver(function ($model) {
return $model->flow_id;
});
// From method
$builder->forceFlowIdResolver(function ($model) {
return $model->resolveFlowId();
});
// From config
$builder->forceFlowIdResolver(function ($model) {
return config('flows.order_flow_id');
});
Use Cases:
- Direct Flow ID binding (HasFlow trait)
- Simplified flow selection
- Explicit flow assignment
- Bypassing complex selection logic
Important: When forceFlowIdResolver is set, all other selection criteria are ignored. Only active constraints (if enabled) are still validated.
Default Requirement
requireDefault()
Require flows with is_default=true. When enabled, only flows marked as default are eligible for selection.
$builder->requireDefault(true); // Only default flows
$builder->requireDefault(false); // Any flow
Parameters:
bool $required = true: Whether to require is_default=true
Returns: $this (fluent interface)
Default: false
Behavior:
- When
true: Filters flows whereis_default = true - When
false: No default requirement (any flow eligible) - Useful for enforcing a single default flow per scope
- Can be relaxed in fallback cascade with
FB_DROP_REQUIRE_DEFAULT
Example:
// Only default flows
$builder->requireDefault(true);
// Any flow (default or not)
$builder->requireDefault(false);
Use Cases:
- Enforcing single default flow
- Default flow selection
- Fallback to default when specific flows unavailable
Candidates Configuration
returnCandidates()
Hint that the consumer intends to fetch candidates (for diagnostics or insight). This is a hint only; pick() still returns a single Flow.
$builder->returnCandidates(true);
Parameters:
bool $on = true: Whether to enable the hint
Returns: $this (fluent interface)
Behavior:
- Hint only - doesn't change
pick()behavior pick()still returns single Flow (first match)- Use
FlowPicker::candidates()to get full list - Useful for debugging and diagnostics
Example:
// Hint for candidate fetching
$builder->returnCandidates(true);
// Get candidates
$candidates = $picker->candidates($model, $builder);
Use Cases:
- Debugging flow selection
- Displaying available flows
- Diagnostics and insight
candidatesLimit()
Limit the number of candidates returned by FlowPicker::candidates(). This helps with performance when fetching candidate lists.
$builder->candidatesLimit(10); // Max 10 candidates
$builder->candidatesLimit(null); // No limit
Parameters:
?int $limit: Positive integer for limit or null for no limit
Returns: $this (fluent interface)
Behavior:
- Applies
LIMITclause to candidate query - Only affects
FlowPicker::candidates(), notpick() - When
null, no limit is applied - When
0or negative, limit is ignored
Example:
// Limit to 10 candidates
$builder->candidatesLimit(10);
// No limit
$builder->candidatesLimit(null);
// Get top 5 candidates
$builder->candidatesLimit(5);
$candidates = $picker->candidates($model, $builder);
Use Cases:
- Performance optimization
- Limiting candidate lists
- Top N flow selection
Performance
cacheInRequest()
Enable simple per-request memoization when no dynamic callbacks are present. This caches flow selection results within a single request for better performance.
$builder->cacheInRequest(true);
Parameters:
bool $on = true: Whether to enable memoization
Returns: $this (fluent interface)
Default: false
Behavior:
- Caches flow selection results per request
- Cache key is computed from builder configuration and model
- Automatically disabled if unsafe conditions detected:
- Custom WHERE callbacks exist
- Custom ordering callback exists
forceFlowIdResolveris set
- Cache is cleared at end of request
When Safe to Enable:
- ✅ No custom WHERE callbacks
- ✅ No custom ordering callback
- ✅ No forceFlowIdResolver
- ✅ Static configuration only
When NOT Safe (automatically disabled):
- ❌ Custom WHERE callbacks (dynamic)
- ❌ Custom ordering callback (dynamic)
- ❌ forceFlowIdResolver (dynamic)
- ❌ Request-dependent logic
Example:
// Safe: Static configuration
$builder->subjectType(Order::class)
->environment('production')
->cacheInRequest(true); // ✅ Safe
// Unsafe: Dynamic callback
$builder->where(function ($query, $model) {
$query->where('field', $model->attr);
})
->cacheInRequest(true); // ❌ Automatically disabled
Use Cases:
- Performance optimization
- Repeated flow selection in same request
- Reducing database queries
Complete Examples
Example 1: Basic Configuration
use JobMetric\Flow\Support\FlowPickerBuilder;
$builder = new FlowPickerBuilder();
$builder->subjectType(Order::class)
->subjectScope((string)$order->tenant_id)
->environment(config('app.env'))
->channel('web')
->onlyActive(true)
->orderByDefault();
Example 2: With Rollout
$builder->subjectType(Order::class)
->environment('production')
->evaluateRollout(true)
->rolloutNamespace('order_v2')
->rolloutSalt('2024')
->rolloutKeyResolver(function ($model) {
return (string)$model->user_id;
});
Example 3: With Fallback
$builder->subjectType(Order::class)
->environment('production')
->channel('web')
->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
]);
Example 4: Version Constraints
$builder->subjectType(Order::class)
->versionAtLeast(1)
->versionAtMost(3); // Versions 1-3
Example 5: Include/Exclude
$builder->subjectType(Order::class)
->includeFlows([1, 2, 3])
->excludeFlows([4, 5])
->preferFlows([1, 2]);
Example 6: Custom Ordering
$builder->subjectType(Order::class)
->orderBy(function ($query) {
$query->orderBy('priority', 'desc')
->orderBy('created_at', 'asc');
});
Example 7: Custom Filters
$builder->subjectType(Order::class)
->where(function ($query, $model) {
if ($model->is_premium) {
$query->where('is_premium_flow', true);
}
});
Example 8: Complex Configuration
$builder->subjectType(Order::class)
->subjectScope((string)$order->tenant_id)
->subjectCollection($order->collection)
->environment(config('app.env'))
->channel(request()->header('X-Channel', 'web'))
->preferEnvironments(['production', 'staging'])
->preferChannels(['web', 'api'])
->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'))
->evaluateRollout(true)
->rolloutNamespace('order')
->rolloutKeyResolver(function ($model) {
return (string)$model->user_id;
})
->versionAtLeast(1)
->versionAtMost(3)
->includeFlows([1, 2, 3])
->excludeFlows([4, 5])
->preferFlows([1, 2])
->orderByDefault()
->matchStrategy(FlowPickerBuilder::STRATEGY_BEST)
->requireDefault(false)
->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
])
->cacheInRequest(true);
Getter Methods
All configuration values can be retrieved:
$builder->getSubjectType();
$builder->getSubjectScope();
$builder->getSubjectCollection();
$builder->getEnvironment();
$builder->getChannel();
$builder->getPreferEnvironments();
$builder->getPreferChannels();
$builder->isOnlyActive();
$builder->shouldIgnoreTimeWindow();
$builder->getNowUtc();
$builder->shouldEvaluateRollout();
$builder->getRolloutNamespace();
$builder->getRolloutSalt();
$builder->getRolloutKeyResolver();
$builder->getVersionEquals();
$builder->getVersionMin();
$builder->getVersionMax();
$builder->getIncludeFlowIds();
$builder->getExcludeFlowIds();
$builder->getPreferFlowIds();
$builder->getMatchStrategy();
$builder->isRequireDefault();
$builder->getFallbackCascade();
$builder->getForceFlowIdResolver();
$builder->shouldReturnCandidates();
$builder->getCandidatesLimit();
$builder->shouldCacheInRequest();
$builder->getWhereCallbacks();
$builder->getOrderingCallback();
Integration with HasWorkflow
The HasWorkflow trait uses FlowPickerBuilder:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
$builder->subjectType(static::class)
->subjectCollection($this->flowSubjectCollection())
->onlyActive(true)
->timeNow(Carbon::now('UTC'))
->orderByDefault()
->evaluateRollout(true)
->rolloutKeyResolver(function (Model $model): ?string {
return $model->getKey() ? (string)$model->getKey() : null;
});
}
Real-World System Implementation Examples
Example 1: Complete E-Commerce Order Management System
A comprehensive order management system with multiple workflows based on order type, customer tier, and environment.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
class Order extends Model
{
use HasWorkflow;
protected $fillable = [
'user_id',
'tenant_id',
'order_type', // 'premium', 'standard', 'express'
'customer_tier', // 'vip', 'regular', 'new'
'total',
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Multi-tenant support
$builder->subjectScope((string)$this->tenant_id);
// Collection-based workflows (order type)
$builder->subjectCollection($this->order_type);
// Environment-specific workflows
$builder->environment(config('app.env'));
// Channel-based workflows
$channel = request()->header('X-Channel', 'web');
$builder->channel($channel);
// Prefer production, then staging
$builder->preferEnvironments(['production', 'staging', 'development']);
// Prefer web, then API
$builder->preferChannels(['web', 'api', 'mobile']);
// Rollout for new workflow (A/B testing)
$builder->evaluateRollout(true)
->rolloutNamespace('order_processing_v2')
->rolloutSalt('2024-01')
->rolloutKeyResolver(function ($model) {
return (string)$model->user_id;
});
// Version constraints (only versions 1-3)
$builder->versionAtLeast(1)
->versionAtMost(3);
// Fallback cascade for graceful degradation
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
FlowPickerBuilder::FB_DISABLE_ROLLOUT,
]);
// Custom filter for customer tier
$builder->where(function ($query, $model) {
$customerTier = $model->customer_tier;
if ($customerTier === 'vip') {
$query->where('is_vip_flow', true);
} elseif ($customerTier === 'new') {
$query->where('is_new_customer_flow', true);
}
});
// Request caching for performance
$builder->cacheInRequest(true);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('order_type');
}
}
Flow Setup:
use JobMetric\Flow\Facades\Flow;
// Premium order flow - Production
Flow::store([
'subject_type' => Order::class,
'subject_scope' => '1', // Tenant 1
'subject_collection' => 'premium',
'environment' => 'production',
'channel' => 'web',
'version' => 2,
'rollout_pct' => 50, // 50% rollout
'is_default' => true,
'status' => true,
]);
// Standard order flow - Production
Flow::store([
'subject_type' => Order::class,
'subject_scope' => '1',
'subject_collection' => 'standard',
'environment' => 'production',
'channel' => 'web',
'version' => 2,
'is_default' => true,
'status' => true,
]);
// Express order flow - Production
Flow::store([
'subject_type' => Order::class,
'subject_scope' => '1',
'subject_collection' => 'express',
'environment' => 'production',
'channel' => 'web',
'version' => 2,
'is_default' => true,
'status' => true,
]);
Usage:
// Create order - flow automatically selected
$order = Order::create([
'user_id' => 123,
'tenant_id' => 1,
'order_type' => 'premium',
'customer_tier' => 'vip',
'total' => 1000.00,
]);
// Flow is automatically bound based on:
// - Tenant ID (subject_scope)
// - Order type (subject_collection)
// - Environment (production)
// - Channel (web)
// - Customer tier (custom filter)
// - Rollout percentage (user_id bucket)
Example 2: Multi-Tenant SaaS Document Management System
A document management system where each tenant has different approval workflows, with environment-specific and time-based activation.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
use Illuminate\Support\Carbon;
class Document extends Model
{
use HasWorkflow;
protected $fillable = [
'tenant_id',
'document_type', // 'contract', 'invoice', 'report'
'department', // 'legal', 'finance', 'hr'
'priority', // 'high', 'normal', 'low'
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Tenant isolation
$builder->subjectScope((string)$this->tenant_id);
// Document type as collection
$builder->subjectCollection($this->document_type);
// Environment-specific
$builder->environment(config('app.env'));
// Active window enforcement
$builder->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'));
// Prefer production, allow staging fallback
$builder->preferEnvironments(['production', 'staging']);
// Version management (only current versions)
$builder->versionAtLeast(1);
// Exclude deprecated flows
$builder->excludeFlows([10, 11, 12]); // Old flow IDs
// Custom filter for department
$builder->where(function ($query, $model) {
$department = $model->department;
// Department-specific flows
if ($department === 'legal') {
$query->where('is_legal_flow', true);
} elseif ($department === 'finance') {
$query->where('is_finance_flow', true);
}
});
// Fallback: drop environment, then ignore time window
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
]);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('document_type');
}
}
Flow Setup:
// Contract approval flow - Tenant 1, Production, Active Jan 2024
Flow::store([
'subject_type' => Document::class,
'subject_scope' => '1',
'subject_collection' => 'contract',
'environment' => 'production',
'version' => 1,
'status' => true,
'active_from' => '2024-01-01 00:00:00',
'active_to' => '2024-12-31 23:59:59',
'is_default' => true,
]);
// Invoice approval flow - Tenant 1, Production
Flow::store([
'subject_type' => Document::class,
'subject_scope' => '1',
'subject_collection' => 'invoice',
'environment' => 'production',
'version' => 1,
'status' => true,
'is_default' => true,
]);
Example 3: Content Management System with A/B Testing
A CMS with gradual rollout of new content approval workflows, supporting multiple content types and channels.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
class Article extends Model
{
use HasWorkflow;
protected $fillable = [
'author_id',
'content_type', // 'news', 'blog', 'press-release'
'category', // 'tech', 'business', 'lifestyle'
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Content type as collection
$builder->subjectCollection($this->content_type);
// Environment
$builder->environment(config('app.env'));
// Channel (web vs admin)
$channel = request()->is('admin/*') ? 'admin' : 'web';
$builder->channel($channel);
// A/B Testing: Rollout new workflow to 30% of authors
$builder->evaluateRollout(true)
->rolloutNamespace('content_approval_v2')
->rolloutSalt('2024-Q1')
->rolloutKeyResolver(function ($model) {
// Consistent assignment by author
return (string)$model->author_id;
});
// Prefer new version, but allow old version
$builder->preferFlows([5, 6]); // New flow IDs
$builder->versionAtLeast(1);
// Fallback: disable rollout if new workflow not available
$builder->fallbackCascade([
FlowPickerBuilder::FB_DISABLE_ROLLOUT,
FlowPickerBuilder::FB_DROP_CHANNEL,
]);
// Custom filter for category
$builder->where(function ($query, $model) {
$category = $model->category;
// Category-specific workflows
if (in_array($category, ['tech', 'business'])) {
$query->where('requires_technical_review', true);
}
});
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('content_type');
}
}
Flow Setup:
// New approval workflow (30% rollout)
Flow::store([
'subject_type' => Article::class,
'subject_collection' => 'news',
'environment' => 'production',
'channel' => 'web',
'version' => 2,
'rollout_pct' => 30, // 30% of authors
'status' => true,
'is_default' => false,
]);
// Old approval workflow (70% of authors, or 100% if rollout disabled)
Flow::store([
'subject_type' => Article::class,
'subject_collection' => 'news',
'environment' => 'production',
'channel' => 'web',
'version' => 1,
'rollout_pct' => null, // No rollout gate
'status' => true,
'is_default' => true,
]);
Example 4: Financial Transaction Processing System
A financial system with strict version control, environment isolation, and scheduled workflow activation.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
use Illuminate\Support\Carbon;
class Transaction extends Model
{
use HasWorkflow;
protected $fillable = [
'account_id',
'transaction_type', // 'payment', 'refund', 'transfer'
'amount',
'currency',
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Transaction type as collection
$builder->subjectCollection($this->transaction_type);
// Strict environment isolation (no fallback)
$builder->environment(config('app.env'));
// Active window enforcement (scheduled workflows)
$builder->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'));
// Version pinning (exact version required)
$currentVersion = config('flows.transaction_version', 2);
$builder->versionEquals($currentVersion);
// Require default flows only
$builder->requireDefault(true);
// No rollout (all transactions use same workflow)
$builder->evaluateRollout(false);
// Custom filter for currency
$builder->where(function ($query, $model) {
$currency = $model->currency;
// Currency-specific workflows
if ($currency === 'USD') {
$query->where('supports_usd', true);
} elseif ($currency === 'EUR') {
$query->where('supports_eur', true);
}
});
// No fallback (strict matching required)
$builder->fallbackCascade([]);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('transaction_type');
}
}
Flow Setup:
// Payment processing flow - Version 2, Production, Active Q1 2024
Flow::store([
'subject_type' => Transaction::class,
'subject_collection' => 'payment',
'environment' => 'production',
'version' => 2,
'status' => true,
'active_from' => '2024-01-01 00:00:00',
'active_to' => '2024-03-31 23:59:59',
'is_default' => true,
'supports_usd' => true,
'supports_eur' => true,
]);
Example 5: Support Ticket System with Priority-Based Workflows
A support ticket system with different workflows based on ticket priority, customer tier, and channel.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
class Ticket extends Model
{
use HasWorkflow;
protected $fillable = [
'customer_id',
'priority', // 'critical', 'high', 'normal', 'low'
'category', // 'technical', 'billing', 'general'
'channel', // 'email', 'chat', 'phone'
'customer_tier', // 'enterprise', 'business', 'standard'
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Priority as collection
$builder->subjectCollection($this->priority);
// Channel-based workflows
$builder->channel($this->channel);
// Environment
$builder->environment(config('app.env'));
// Prefer critical priority flows
$builder->preferFlows([1, 2, 3]); // Critical flow IDs
// Version constraints
$builder->versionAtLeast(1)
->versionAtMost(2);
// Custom filters
$builder->where(function ($query, $model) {
// Category-specific
if ($model->category === 'technical') {
$query->where('requires_technical_team', true);
}
// Customer tier
if ($model->customer_tier === 'enterprise') {
$query->where('is_enterprise_flow', true);
}
});
// Fallback: drop channel, then environment
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
]);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('priority');
}
}
Flow Setup:
// Critical priority - Email channel
Flow::store([
'subject_type' => Ticket::class,
'subject_collection' => 'critical',
'channel' => 'email',
'environment' => 'production',
'version' => 1,
'requires_technical_team' => true,
'status' => true,
'is_default' => true,
]);
// High priority - Chat channel
Flow::store([
'subject_type' => Ticket::class,
'subject_collection' => 'high',
'channel' => 'chat',
'environment' => 'production',
'version' => 1,
'status' => true,
'is_default' => true,
]);
Example 6: Invoice Processing with Time-Based Activation
An invoice processing system with workflows that activate/deactivate at specific times, supporting multiple currencies and regions.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
use Illuminate\Support\Carbon;
class Invoice extends Model
{
use HasWorkflow;
protected $fillable = [
'tenant_id',
'region', // 'us', 'eu', 'asia'
'currency',
'invoice_type', // 'recurring', 'one-time'
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Tenant isolation
$builder->subjectScope((string)$this->tenant_id);
// Invoice type as collection
$builder->subjectCollection($this->invoice_type);
// Environment
$builder->environment(config('app.env'));
// Strict time window enforcement
$builder->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'));
// Prefer current region
$builder->preferEnvironments(['production', 'staging']);
// Version management
$builder->versionAtLeast(1);
// Custom filters
$builder->where(function ($query, $model) {
// Region-specific
$query->where('supported_regions', 'LIKE', "%{$model->region}%");
// Currency support
if ($model->currency === 'USD') {
$query->where('supports_usd', true);
}
});
// Fallback: ignore time window if outside active period
$builder->fallbackCascade([
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
]);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('invoice_type');
}
}
Flow Setup:
// Recurring invoice flow - Active during business quarter
Flow::store([
'subject_type' => Invoice::class,
'subject_scope' => '1',
'subject_collection' => 'recurring',
'environment' => 'production',
'version' => 1,
'status' => true,
'active_from' => '2024-01-01 00:00:00',
'active_to' => '2024-03-31 23:59:59',
'supported_regions' => 'us,eu,asia',
'supports_usd' => true,
'is_default' => true,
]);
Example 7: Complete Implementation with All Features
A comprehensive example using all FlowPickerBuilder features for a complex enterprise system.
Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use JobMetric\Flow\HasWorkflow;
use JobMetric\Flow\Support\FlowPickerBuilder;
use Illuminate\Support\Carbon;
class EnterpriseOrder extends Model
{
use HasWorkflow;
protected $fillable = [
'organization_id',
'department_id',
'order_category', // 'procurement', 'it', 'hr'
'order_type', // 'urgent', 'standard', 'planned'
'budget_tier', // 'high', 'medium', 'low'
'region',
'status',
];
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Organization scope (multi-tenant)
$builder->subjectScope((string)$this->organization_id);
// Order category as collection
$builder->subjectCollection($this->order_category);
// Environment with preference
$builder->environment(config('app.env'))
->preferEnvironments(['production', 'staging', 'development']);
// Channel
$channel = $this->determineChannel();
$builder->channel($channel)
->preferChannels(['web', 'api', 'mobile']);
// Active constraints with time window
$builder->onlyActive(true)
->ignoreTimeWindow(false)
->timeNow(Carbon::now('UTC'));
// Rollout for new workflow (25% of organizations)
$builder->evaluateRollout(true)
->rolloutNamespace('enterprise_order_v3')
->rolloutSalt('2024-Q2')
->rolloutKeyResolver(function ($model) {
return (string)$model->organization_id;
});
// Version constraints
$builder->versionAtLeast(2)
->versionAtMost(4);
// Include only approved flows
$builder->includeFlows([10, 11, 12, 13]);
// Exclude deprecated flows
$builder->excludeFlows([1, 2, 3]);
// Prefer newer flows
$builder->preferFlows([13, 12, 11, 10]);
// Require default flows
$builder->requireDefault(true);
// Custom ordering
$builder->orderBy(function ($query) {
$query->orderByDesc('priority')
->orderByDesc('version')
->orderByDesc('is_default');
});
// Match strategy
$builder->matchStrategy(FlowPickerBuilder::STRATEGY_BEST);
// Custom filters
$builder->where(function ($query, $model) {
// Department-specific
if ($model->department_id === 5) { // IT Department
$query->where('requires_it_approval', true);
}
// Budget tier
if ($model->budget_tier === 'high') {
$query->where('requires_executive_approval', true);
}
// Region
$query->where('supported_regions', 'LIKE', "%{$model->region}%");
});
// Comprehensive fallback cascade
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
FlowPickerBuilder::FB_DISABLE_ROLLOUT,
FlowPickerBuilder::FB_DROP_REQUIRE_DEFAULT,
]);
// Performance optimization
$builder->cacheInRequest(true);
}
protected function flowSubjectCollection(): ?string
{
return $this->getAttribute('order_category');
}
protected function determineChannel(): string
{
if (request()->is('api/*')) {
return 'api';
} elseif (request()->is('mobile/*')) {
return 'mobile';
}
return 'web';
}
}
Complete Flow Setup:
use JobMetric\Flow\Facades\Flow;
// Procurement flow - Production, Version 3, 25% rollout
Flow::store([
'subject_type' => EnterpriseOrder::class,
'subject_scope' => '100', // Organization 100
'subject_collection' => 'procurement',
'environment' => 'production',
'channel' => 'web',
'version' => 3,
'rollout_pct' => 25,
'status' => true,
'active_from' => '2024-04-01 00:00:00',
'active_to' => '2024-06-30 23:59:59',
'is_default' => true,
'priority' => 10,
'requires_it_approval' => false,
'requires_executive_approval' => true,
'supported_regions' => 'us,eu',
]);
// IT flow - Production, Version 3
Flow::store([
'subject_type' => EnterpriseOrder::class,
'subject_scope' => '100',
'subject_collection' => 'it',
'environment' => 'production',
'channel' => 'web',
'version' => 3,
'rollout_pct' => 25,
'status' => true,
'active_from' => '2024-04-01 00:00:00',
'active_to' => '2024-06-30 23:59:59',
'is_default' => true,
'priority' => 10,
'requires_it_approval' => true,
'requires_executive_approval' => false,
'supported_regions' => 'us,eu,asia',
]);
Usage in Controller:
namespace App\Http\Controllers;
use App\Models\EnterpriseOrder;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function store(Request $request)
{
// Create order - flow automatically selected
$order = EnterpriseOrder::create([
'organization_id' => $request->organization_id,
'department_id' => $request->department_id,
'order_category' => $request->category,
'order_type' => $request->type,
'budget_tier' => $request->budget_tier,
'region' => $request->region,
]);
// Flow is automatically bound with all criteria:
// - Organization scope
// - Order category
// - Environment
// - Channel
// - Active time window
// - Rollout percentage
// - Version constraints
// - Custom filters (department, budget, region)
return response()->json([
'order' => $order,
'flow' => $order->boundFlow(),
]);
}
}
Example 8: Testing and Development Scenarios
Examples for testing and development environments.
Test Configuration:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Testing: Include specific test flows
if (app()->environment('testing')) {
$builder->includeFlows([999, 998, 997]) // Test flow IDs
->evaluateRollout(false) // Disable rollout in tests
->onlyActive(false) // Include inactive flows
->ignoreTimeWindow(true); // Ignore time windows
} else {
// Production configuration
$builder->environment(config('app.env'))
->onlyActive(true)
->evaluateRollout(true);
}
}
Development Configuration:
protected function buildFlowPicker(FlowPickerBuilder $builder): void
{
parent::buildFlowPicker($builder);
// Development: Allow any environment, prefer development
$builder->environment(null) // No environment filter
->preferEnvironments(['development', 'staging', 'production'])
->onlyActive(false) // Include inactive for testing
->fallbackCascade([
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
]);
}
Related Documentation
- FlowPicker - Flow selection mechanism
- HasWorkflow - Workflow integration trait
- Flow Service - Flow management
- HasFlow - Simple flow binding