Skip to main content

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:

  1. HasWorkflow Trait Integration

    • When models use HasWorkflow trait, they override buildFlowPicker() method
    • This method receives a FlowPickerBuilder instance to configure flow selection
    • Used automatically during model creation to select the appropriate flow
  2. HasFlow Trait Integration

    • When models use HasFlow trait, they also use buildFlowPicker() internally
    • Uses forceFlowIdResolver to bypass complex selection logic
    • Directly binds flows by ID while still using the builder pattern
  3. 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
  4. 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 HasFlow trait 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_type column matches the provided class
  • Must be set before flow selection can work
  • Used internally by HasWorkflow and HasFlow traits

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_scope column 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_collection column 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 environment column 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 channel column 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: Requires status = true and optionally checks active_from/active_to time 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 checks status = true, ignores active_from and active_to
  • When false: Checks both status = true and 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_from and active_to constraints
  • 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: Ignores rollout_pct column, all flows are eligible
  • Requires rolloutKeyResolver to be set for proper operation
  • If no rollout key is available, only flows with rollout_pct IS NULL are 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 null or empty string, only flows with rollout_pct IS NULL are 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 version column equals the provided value
  • Disables versionMin and versionMax if 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 versionEquals is set
  • Can be combined with versionAtMost for 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 versionEquals is set
  • Can be combined with versionAtLeast for 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 as versionAtLeast)
  • versionMax(int $max): Maximum version (same as versionAtMost)

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):

  1. version DESC - Higher versions preferred
  2. is_default DESC - Default flows preferred
  3. ordering DESC - Higher ordering value preferred
  4. id DESC - Higher ID as tiebreaker

Behavior:

  • Sets the orderingCallback to default implementation
  • Used automatically if no custom orderBy() is set
  • Only applies when matchStrategy is STRATEGY_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 $query parameter
  • Can apply any ordering logic
  • Only used when matchStrategy is STRATEGY_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_BEST or STRATEGY_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 to STRATEGY_FIRST
  • When false: Sets strategy to STRATEGY_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 $query and Model $model parameters
  • 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:

  1. FB_DROP_CHANNEL: Remove the channel filter

    • Allows flows from any channel
    • Useful when channel-specific flows aren't available
  2. FB_DROP_ENVIRONMENT: Remove the environment filter

    • Allows flows from any environment
    • Useful when environment-specific flows aren't available
  3. FB_IGNORE_TIMEWINDOW: Ignore active_from/active_to checks

    • Keeps status=true requirement
    • Useful when flows are outside active window
  4. FB_DISABLE_ROLLOUT: Disable rollout gating entirely

    • Ignores rollout_pct column
    • Useful when rollout prevents matching
  5. FB_DROP_REQUIRE_DEFAULT: Don't require is_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 where is_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 LIMIT clause to candidate query
  • Only affects FlowPicker::candidates(), not pick()
  • When null, no limit is applied
  • When 0 or 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
    • forceFlowIdResolver is 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,
]);
}