Strategies

Control how handler results are aggregated

Overview

Strategies determine how results from multiple handlers are combined. By default, handlers execute but their return values are ignored. Strategies let you collect, merge, or reduce results.

Available Strategies

Strategy Purpose Returns
FirstResult Get first non-null result Single value
MergeResults Combine all results Array or Collection
ReduceResults Reduce to single value Computed value

FirstResult Strategy

Returns the first non-null handler result and stops execution:

use Esegments\LaravelExtensions\Facades\Extensions;

// First handler to return a non-null value wins
$price = Extensions::firstResult()->dispatch(new CalculatePrice($product));

Example

// Handler 1: Returns null (skipped)
class DefaultPriceHandler {
    public function handle($ext): ?float {
        return null; // No opinion
    }
}

// Handler 2: Returns value (used)
class DiscountPriceHandler {
    public function handle($ext): ?float {
        if ($ext->product->hasDiscount()) {
            return $ext->product->discounted_price;
        }
        return null;
    }
}

// Handler 3: Never reached (first result already found)
class FallbackPriceHandler {
    public function handle($ext): float {
        return $ext->product->base_price;
    }
}

MergeResults Strategy

Collects all handler results into an array or collection:

// As array
$errors = Extensions::mergeResults()
    ->dispatch(new ValidateOrder($order));

// As collection
$errors = Extensions::mergeResults(asCollection: true)
    ->dispatch(new ValidateOrder($order));

// Flatten nested arrays
$errors = Extensions::mergeResults(flattenArrays: true)
    ->dispatch(new ValidateOrder($order));

Example: Collecting Validation Errors

class CheckInventoryHandler {
    public function handle($ext): array {
        $errors = [];
        foreach ($ext->order->items as $item) {
            if (!$item->product->inStock($item->quantity)) {
                $errors[] = "{$item->product->name} is out of stock";
            }
        }
        return $errors;
    }
}

class CheckAddressHandler {
    public function handle($ext): array {
        if (!$ext->order->shippingAddress->isValid()) {
            return ['Invalid shipping address'];
        }
        return [];
    }
}

// Dispatch
$allErrors = Extensions::mergeResults(flattenArrays: true)
    ->dispatch(new ValidateOrder($order));

if (!empty($allErrors)) {
    return back()->withErrors($allErrors);
}

Fluent API

Extensions::mergeResults()
    ->asCollection()      // Return as Collection
    ->preserveNesting()   // Don't flatten arrays
    ->dispatch($extension);

ReduceResults Strategy

Reduces all results using a callback function:

// Sum all results
$total = Extensions::reduceResults(
    fn ($carry, $result) => $carry + $result,
    initial: 0
)->dispatch(new CalculateOrderTotal($order));

Built-in Reducers

use Esegments\LaravelExtensions\Strategies\ReduceResultsStrategy;

// Sum numbers
$total = Extensions::reduceResults(ReduceResultsStrategy::sum())
    ->dispatch($extension);

// Check all true
$allValid = Extensions::reduceResults(ReduceResultsStrategy::allTrue())
    ->dispatch(new ValidateAll($data));

// Check any true
$hasPermission = Extensions::reduceResults(ReduceResultsStrategy::anyTrue())
    ->dispatch(new CheckPermissions($user, $resource));

// Count results
$count = Extensions::reduceResults(ReduceResultsStrategy::count())
    ->dispatch($extension);

// Find minimum
$min = Extensions::reduceResults(ReduceResultsStrategy::min())
    ->dispatch(new GetPriceQuotes($product));

// Find maximum
$max = Extensions::reduceResults(ReduceResultsStrategy::max())
    ->dispatch(new GetBidAmounts($auction));

Custom Reducers

// Concatenate strings
$combined = Extensions::reduceResults(
    fn ($carry, $result) => $carry . $result,
    initial: ''
)->dispatch($extension);

// Collect into array with transformation
$transformed = Extensions::reduceResults(
    fn ($carry, $result) => [...$carry, strtoupper($result)],
    initial: []
)->dispatch($extension);

// Complex reduction
$stats = Extensions::reduceResults(
    function ($carry, $result) {
        $carry['total'] += $result['amount'];
        $carry['count']++;
        return $carry;
    },
    initial: ['total' => 0, 'count' => 0]
)->dispatch($extension);

Combining with Other Features

With Graceful Mode

$result = Extensions::gracefully()
    ->mergeResults()
    ->dispatch($extension);

// Check for errors
if ($result->hasErrors()) {
    Log::error('Some handlers failed', $result->errors());
}

With Circuit Breaker

Strategies work with circuit breaker automatically - handlers with open circuits are skipped:

$prices = Extensions::mergeResults()
    ->dispatch(new GetPriceQuotes($product));

// Handlers with open circuits are skipped, not included in results

Creating Custom Strategies

Implement ResultStrategyContract:

use Esegments\LaravelExtensions\Contracts\ResultStrategyContract;

class AverageResultsStrategy implements ResultStrategyContract
{
    private array $results = [];

    public function collect(mixed $result): void
    {
        if (is_numeric($result)) {
            $this->results[] = $result;
        }
    }

    public function getResult(): float
    {
        if (empty($this->results)) {
            return 0;
        }
        return array_sum($this->results) / count($this->results);
    }
}

// Usage
$average = Extensions::withStrategy(new AverageResultsStrategy())
    ->dispatch(new GetRatings($product));