Skip to main content

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:

  1. Check Forced Flow: If forceFlowIdResolver is set, resolve and validate it
  2. Check Cache: If request caching is enabled, return cached result
  3. Get Candidates: Query flows matching builder criteria
  4. Apply Fallback: If no candidates found, apply fallback cascade
  5. Cache Result: Store result in request cache if enabled
  6. Return Flow: Return the first matching flow or null

Forced Flow Resolution

If forceFlowIdResolver is configured, the picker will:

  1. Call the resolver to get Flow ID
  2. Load the Flow from database
  3. Validate active constraints (if enabled)
  4. 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 forceFlowIdResolver is 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_from and active_to time 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):

  1. Get rollout key from rolloutKeyResolver
  2. Compute stable bucket (0-99) using namespace, salt, and key
  3. Filter flows where rollout_pct >= bucket or rollout_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:

  1. version DESC - Higher versions preferred
  2. is_default DESC - Default flows preferred
  3. ordering DESC - Higher ordering preferred
  4. id 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:

  1. FB_DROP_CHANNEL: Remove channel filter
  2. FB_DROP_ENVIRONMENT: Remove environment filter
  3. FB_IGNORE_TIMEWINDOW: Ignore active window checks
  4. FB_DISABLE_ROLLOUT: Disable rollout checks
  5. 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 candidatesLimit is set

Integration with HasWorkflow

The HasWorkflow trait uses FlowPicker internally:

// Inside HasWorkflow::pickFlow()
$builder = $this->makeFlowPicker();
$flow = (new FlowPicker())->pick($this, $builder);