FlowPicker
The FlowPicker class executes the flow selection process using a configured FlowPickerBuilder. It handles filtering, active constraints, rollout gates, version constraints, and fallback cascades to select the most appropriate flow for a model instance.
Namespace
JobMetric\Flow\Support\FlowPicker
Overview
The FlowPicker is responsible for:
- Filtering flows by subject type/scope/collection, environment, channel
- Enforcing active/time-window constraints (configurable)
- Applying rollout gates (configurable)
- Supporting include/exclude/prefer lists and version constraints
- Honoring match strategy (BEST/FIRST) and fallback cascade
- Providing a
candidates()helper for diagnostics/insight - Per-request caching for performance
Basic Usage
Pick a Flow
Select a single flow according to the builder's strategy:
use JobMetric\Flow\Support\FlowPicker;
use JobMetric\Flow\Support\FlowPickerBuilder;
$picker = new FlowPicker();
$builder = new FlowPickerBuilder();
// Configure builder
$builder->subjectType(Order::class)
->environment('production')
->channel('web')
->onlyActive(true);
// Pick flow
$flow = $picker->pick($order, $builder);
Returns: Flow|null - The selected flow or null if no match found
Get Candidates
Get all matching flows without applying fallback cascade:
$picker = new FlowPicker();
$candidates = $picker->candidates($order, $builder);
Returns: Collection<int, Flow> - Collection of matching flows
Use Cases:
- Debugging flow selection
- Displaying available flows
- Diagnostics and insight
Selection Process
The pick() method follows this process:
- Check Forced Flow: If
forceFlowIdResolveris set, resolve and validate it - Check Cache: If request caching is enabled, return cached result
- Get Candidates: Query flows matching builder criteria
- Apply Fallback: If no candidates found, apply fallback cascade
- Cache Result: Store result in request cache if enabled
- Return Flow: Return the first matching flow or null
Forced Flow Resolution
If forceFlowIdResolver is configured, the picker will:
- Call the resolver to get Flow ID
- Load the Flow from database
- Validate active constraints (if enabled)
- Return the Flow if valid, null otherwise
This is used by HasFlow trait for direct Flow ID binding.
Request Caching
The picker supports per-request memoization for performance:
$builder->cacheInRequest(true);
$flow = $picker->pick($order, $builder);
Cache Key Components:
- Model class and ID
- Subject type, scope, collection
- Environment, channel
- Active constraints
- Rollout configuration
- Version constraints
- Include/exclude IDs
Cache Safety:
- Only enabled when builder has no dynamic callbacks
- Automatically disabled if custom WHERE callbacks exist
- Automatically disabled if custom ordering callback exists
- Automatically disabled if
forceFlowIdResolveris set
Filtering
Subject Filters
Flows are filtered by:
- Subject Type: Model class name (required)
- Subject Scope: Optional tenant/org identifier
- Subject Collection: Optional collection identifier (e.g., 'premium', 'standard')
Environment and Channel
- Environment: Filter by environment (e.g., 'production', 'staging')
- Channel: Filter by channel (e.g., 'web', 'api', 'mobile')
Active Constraints
When onlyActive(true):
- Requires
status = true - Optionally checks
active_fromandactive_totime windows - Can ignore time window with
ignoreTimeWindow(true)
Version Constraints
- Exact Version:
versionEquals(2)- Only version 2 - Version Range:
versionMin(1)->versionMax(3)- Versions 1-3
Include/Exclude Lists
- Include IDs: Only flows with these IDs
- Exclude IDs: Exclude flows with these IDs
Default Requirement
- Require Default: Only flows with
is_default = true
Rollout Gating
When evaluateRollout(true):
- Get rollout key from
rolloutKeyResolver - Compute stable bucket (0-99) using namespace, salt, and key
- Filter flows where
rollout_pct >= bucketorrollout_pct IS NULL
Stable Bucket Algorithm:
- Combines namespace, salt, and key
- Uses CRC32 hash
- Returns bucket in range 0-99
Example:
// Flow with rollout_pct: 50
// User with bucket: 30 → Matches (30 < 50)
// User with bucket: 60 → Doesn't match (60 >= 50)
Ordering
Match Strategy
- STRATEGY_BEST: Returns best candidate based on ordering rules (default)
- STRATEGY_FIRST: Returns first matching record (minimal ordering)
Default Ordering
When using STRATEGY_BEST:
version DESC- Higher versions preferredis_default DESC- Default flows preferredordering DESC- Higher ordering preferredid DESC- Higher ID as tiebreaker
Preferential Ordering
Boosts (not filters) for:
- Preferred Flow IDs: Earlier IDs rank higher
- Preferred Environments: Earlier environments rank higher
- Preferred Channels: Earlier channels rank higher
Fallback Cascade
If no flow matches initially, the picker applies fallback steps:
- FB_DROP_CHANNEL: Remove channel filter
- FB_DROP_ENVIRONMENT: Remove environment filter
- FB_IGNORE_TIMEWINDOW: Ignore active window checks
- FB_DISABLE_ROLLOUT: Disable rollout checks
- FB_DROP_REQUIRE_DEFAULT: Don't require is_default
Example:
$builder->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
]);
The picker tries each step in order until a flow is found.
Custom Filters
You can add custom WHERE callbacks:
$builder->where(function ($query, $model) {
$query->where('custom_field', $model->some_attribute);
});
These are applied after all standard filters.
Complete Examples
Example 1: Basic Flow Selection
use JobMetric\Flow\Support\FlowPicker;
use JobMetric\Flow\Support\FlowPickerBuilder;
$picker = new FlowPicker();
$builder = new FlowPickerBuilder();
$builder->subjectType(Order::class)
->subjectScope((string)$order->tenant_id)
->environment(config('app.env'))
->channel('web')
->onlyActive(true)
->orderByDefault();
$flow = $picker->pick($order, $builder);
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;
});
$flow = $picker->pick($order, $builder);
Example 3: With Fallback Cascade
$builder->subjectType(Order::class)
->environment('production')
->channel('web')
->fallbackCascade([
FlowPickerBuilder::FB_DROP_CHANNEL,
FlowPickerBuilder::FB_DROP_ENVIRONMENT,
FlowPickerBuilder::FB_IGNORE_TIMEWINDOW,
]);
$flow = $picker->pick($order, $builder);
Example 4: Get All Candidates
$builder->subjectType(Order::class)
->environment('production');
$candidates = $picker->candidates($order, $builder);
// Display all matching flows
foreach ($candidates as $flow) {
echo "Flow ID: {$flow->id}, Version: {$flow->version}\n";
}
Example 5: With Version Constraints
$builder->subjectType(Order::class)
->versionMin(1)
->versionMax(3); // Only versions 1-3
$flow = $picker->pick($order, $builder);
Example 6: With Include/Exclude
$builder->subjectType(Order::class)
->includeFlowIds([1, 2, 3]) // Only these flows
->excludeFlowIds([4, 5]) // Exclude these
->preferFlowIds([1, 2]); // Prefer these
$flow = $picker->pick($order, $builder);
Performance Considerations
Request Caching
Enable caching for better performance:
$builder->cacheInRequest(true);
$flow = $picker->pick($order, $builder);
When Safe:
- No custom WHERE callbacks
- No custom ordering callback
- No forceFlowIdResolver
When Not Safe:
- Dynamic callbacks that depend on request state
- Callbacks that access
$request,auth(), etc.
Query Optimization
The picker automatically:
- Uses indexed columns (subject_type, environment, channel)
- Applies efficient WHERE clauses
- Uses proper JOINs when needed
- Limits results when
candidatesLimitis set
Integration with HasWorkflow
The HasWorkflow trait uses FlowPicker internally:
// Inside HasWorkflow::pickFlow()
$builder = $this->makeFlowPicker();
$flow = (new FlowPicker())->pick($this, $builder);
Related Documentation
- FlowPickerBuilder - Configuring flow selection
- HasWorkflow - Workflow integration trait
- Flow Service - Flow management
- HasFlow - Simple flow binding