PHP code example of gregpriday / laravel-retry

1. Go to this page and download the library: Download gregpriday/laravel-retry library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

gregpriday / laravel-retry example snippets


'providers' => [
    // ...
    GregPriday\LaravelRetry\RetryServiceProvider::class,
],

'aliases' => [
    // ...
    'Retry' => GregPriday\LaravelRetry\Facades\Retry::class,
],

use GregPriday\LaravelRetry\Facades\Retry;
use Illuminate\Support\Facades\Http;

// Simple retry with default settings
$data = Retry::run(function () {
    $response = Http::get('https://api.example.com/data');
    $response->throw(); // Will automatically retry on network errors & 5xx responses
    return $response->json();
})->value();

// With HTTP Client macro (even simpler)
$response = Http::robustRetry(3)  // Max 3 attempts total (1 initial + 2 retries)
    ->get('https://api.example.com/resource');

// If no strategy is explicitly provided, robustRetry uses GuzzleResponseStrategy as the base.
// When options are provided without a strategy, the GuzzleResponseStrategy is wrapped with
// CustomOptionsStrategy to apply those settings.

use GregPriday\LaravelRetry\Facades\Retry;
use Illuminate\Support\Facades\Http;

$result = Retry::run(function () {
        $response = Http::get('https://api.example.com/data');
        $response->throw();
        return $response->json();
    })
    ->then(function ($data) {
        return ['status' => 'success', 'payload' => $data];
    })
    ->catch(function (Throwable $e) {
        return ['status' => 'failed', 'error' => $e->getMessage()];
    });

$result = Retry::maxRetries(5)    // Override default max retries
    ->timeout(10)                 // Override default timeout per attempt (seconds)
    ->withStrategy(new ExponentialBackoffStrategy(baseDelay: 0.5)) // Configure strategy with custom base delay
    ->run(function () {
        // Your operation here
    })
    ->value();                    // Get result directly or throws the final exception

use GregPriday\LaravelRetry\Facades\Retry;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

$lockKey = 'api-operation-lock';

$result = Retry::maxRetries(3)
    ->run(function () {
        // Set a lock to prevent concurrent operations
        Cache::put($lockKey, true, 60);
        
        // Operation that might fail
        $response = Http::get('https://api.example.com/data');
        $response->throw();
        return $response->json();
    })
    ->then(function ($data) {
        return ['status' => 'success', 'data' => $data];
    })
    ->catch(function (Throwable $e) {
        return ['status' => 'failed', 'error' => $e->getMessage()];
    })
    ->finally(function () use ($lockKey) {
        // This always executes regardless of success or failure
        // Perfect for cleanup operations
        Cache::forget($lockKey);
        Log::info('Operation completed, lock released');
    })
    ->value();

$processedData = Retry::maxRetries(3)
    ->run(function () {
        return DB::transaction(function () {
            // Complex database operation that might fail
            return processSensitiveData();
        });
    })
    ->then(function ($data) {
        // Process successful result
        Log::info('Data processed successfully', ['record_count' => count($data)]);
        return $data;
    })
    ->catch(function (QueryException $e) {
        // Handle database-specific errors
        Log::error('Database error during processing', ['error' => $e->getMessage()]);
        return ['error' => 'Database operation failed', 'retry_allowed' => true];
    })
    ->catch(function (Throwable $e) {
        // Handle any other exceptions
        Log::critical('Unexpected error during processing', ['error' => $e->getMessage()]);
        return ['error' => 'Processing failed', 'retry_allowed' => false];
    })
    ->finally(function () {
        // Clean up resources, always executed
        Cache::forget('processing_lock');
    })
    ->value(); // Get the final result (or throw the exception)

use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;
use GregPriday\LaravelRetry\Strategies\FixedDelayStrategy;
use GregPriday\LaravelRetry\Strategies\LinearBackoffStrategy;

// With ExponentialBackoffStrategy - Overrides config default for this instance
// Base delay: 1.5s, then ~3s, then ~6s with jitter
Retry::withStrategy(new ExponentialBackoffStrategy(baseDelay: 1.5))
    ->run($operation);

// With FixedDelayStrategy - Overrides config default for this instance
// Every retry will wait 0.5s (plus jitter if enabled)
Retry::withStrategy(new FixedDelayStrategy(baseDelay: 0.5))
    ->run($operation);

// With LinearBackoffStrategy (increment: 1.5) - Overrides config default for this instance
// First retry after 1s, then 2.5s, then 4s
Retry::withStrategy(new LinearBackoffStrategy(baseDelay: 1.0, increment: 1.5))
    ->run($operation);

use Illuminate\Support\Facades\Http;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

// Simple robust retry with default settings
$response = Http::robustRetry(3)  // Max 3 attempts total (1 initial + 2 retries)
    ->get('https://api.example.com/resource');

// Using a custom retry strategy
$strategy = new ExponentialBackoffStrategy(
    multiplier: 1.5,
    maxDelay: 60,
    withJitter: true
);

$response = Http::withRetryStrategy($strategy, [
        'max_attempts' => 5,
        'base_delay' => 2.5,  // Configures the baseDelay parameter of the underlying strategy
        'timeout' => 15,
    ])
    ->post('https://api.example.com/submit', ['foo' => 'bar']);

// Custom retry conditions
$response = Http::retryWhen(
    function ($attempt, $maxAttempts, $exception, $options) {
        return $attempt < $maxAttempts &&
               $exception instanceof RequestException &&
               $exception->response->status() === 503;
    },
    [
        'max_attempts' => 4,
        'timeout' => 10,
        'base_delay' => 0.75,  // Configures the baseDelay parameter of the underlying strategy
    ]
)->get('https://api.example.com/data');

use Illuminate\Support\Facades\Http;

// Apply circuit breaker with default parameters
// Uses GuzzleResponseStrategy as the default inner strategy
$response = Http::withCircuitBreaker()
    ->get('https://api.example.com/endpoint');

// Apply circuit breaker with custom parameters
// The base_delay configures the underlying inner strategy (GuzzleResponseStrategy)
$response = Http::withCircuitBreaker(
    failureThreshold: 5,    // Open after 5 failures
    resetTimeout: 60,       // Try again after 60 seconds
    [
        'max_attempts' => 3,
        'base_delay' => 1.0, // Configures the baseDelay of the inner strategy
        'timeout' => 5,
    ]
)->post('https://api.example.com/data', ['key' => 'value']);

use Illuminate\Support\Facades\Http;

// Apply rate limiting with default parameters
$response = Http::withRateLimitHandling()
    ->get('https://api.example.com/endpoint');
    
// Apply rate limiting with custom parameters
$response = Http::withRateLimitHandling(
    maxAttempts: 100,      // Max 100 requests
    timeWindow: 60,        // Per minute
    storageKey: 'api-rate-limit',
    [
        'max_attempts' => 3,
        'base_delay' => 1.0,
    ]
)->get('https://api.example.com/data');

use Illuminate\Support\Facades\Http;

// Combining features
$response = Http::withToken('api-token')
    ->withCircuitBreaker(5, 60)
    ->withHeaders(['Custom-Header' => 'Value'])
    ->timeout(5)
    ->get('https://api.example.com/protected-endpoint');

use GregPriday\LaravelRetry\Facades\RetryablePipeline;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;
use GregPriday\LaravelRetry\Contracts\RetryStrategy;
use GregPriday\LaravelRetry\Strategies\FixedDelayStrategy;

// Define pipeline stages with custom retry configurations
class ValidateDataStage
{
    // Uses pipeline default settings
    public function handle($data, $next)
    {
        // Validation logic
        return $next($data);
    }
}

class ProcessDataStage
{
    public int $retryCount = 4;  // Override pipeline default retries
    public RetryStrategy $retryStrategy;  // Custom retry strategy for this stage

    public function __construct()
    {
        // Configure baseDelay in the strategy constructor
        $this->retryStrategy = new ExponentialBackoffStrategy(
            baseDelay: 2.0,
            multiplier: 1.5
        );
    }

    public function handle($data, $next)
    {
        // Process data here
        return $next($data);
    }
}

class SaveResultsStage
{
    public int $retryCount = 1;  // Only 1 retry for saving
    public int $timeout = 10;    // 10-second timeout per attempt
    
    public function handle($data, $next)
    {
        // Save results logic
        return $next($data);
    }
}

$result = RetryablePipeline::maxRetries(2)
    ->withStrategy(new FixedDelayStrategy(baseDelay: 0.5)) // Default strategy for all stages
    ->send(['initial' => 'data'])
    ->through([
        new ValidateDataStage(),     // Uses pipeline defaults (2 retries, FixedDelay)
        new ProcessDataStage(),      // Uses its own settings (4 retries, ExponentialBackoff)
        new SaveResultsStage(),      // Uses its own settings (1 retry, 10s timeout)
    ])
    ->then(function ($processedData) {
        return $processedData;
    });

use GregPriday\LaravelRetry\Facades\Retry;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

Retry::withStrategy(new ExponentialBackoffStrategy())
    ->run(function () {
        // Your operation here
    });

// Using the class directly
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

$strategy = new ExponentialBackoffStrategy(
    baseDelay: 1.0,       // Initial value used for delay calculation (default: 1.0)
    multiplier: 2.0,      // Delay multiplier
    maxDelay: 60,         // Maximum delay in seconds
    withJitter: true,     // Add randomness to prevent thundering herd
    jitterPercent: 0.2    // Percentage of jitter (0.2 means ±20%)
);

// Using the factory with options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('exponential-backoff', [
    'baseDelay' => 1.0,
    'multiplier' => 2.0,
    'maxDelay' => 60,
    'withJitter' => true,
    'jitterPercent' => 0.2
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\LinearBackoffStrategy;

$strategy = new LinearBackoffStrategy(
    baseDelay: 1.0,    // Base delay in seconds (default: 1.0)
    increment: 5,      // Add 5 seconds each retry
    maxDelay: 30       // Cap at 30 seconds
);

// Using the factory with options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('linear-backoff', [
    'baseDelay' => 1.0,
    'increment' => 5,
    'maxDelay' => 30
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\FibonacciBackoffStrategy;

$strategy = new FibonacciBackoffStrategy(
    baseDelay: 1.0,      // Base delay in seconds (default: 1.0)
    maxDelay: 60,        // Maximum delay in seconds
    withJitter: true,    // Add randomness to prevent thundering herd
    jitterPercent: 0.2   // Percentage of jitter (0.2 means ±20%)
);

// Using the factory with options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('fibonacci-backoff', [
    'baseDelay' => 1.0,
    'maxDelay' => 60,
    'withJitter' => true,
    'jitterPercent' => 0.2
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\FixedDelayStrategy;

$strategy = new FixedDelayStrategy(
    baseDelay: 1.0,       // Fixed delay between all retries (default: 1.0)
    withJitter: true,     // Add randomness
    jitterPercent: 0.2    // ±20% jitter
);

// Using the factory with options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('fixed-delay', [
    'baseDelay' => 1.0,
    'withJitter' => true,
    'jitterPercent' => 0.2
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\DecorrelatedJitterStrategy;

$strategy = new DecorrelatedJitterStrategy(
    baseDelay: 1.0,     // Base delay in seconds (default: 1.0)
    maxDelay: 60,       // Maximum delay
    minFactor: 1.0,     // Minimum delay multiplier
    maxFactor: 3.0      // Maximum delay multiplier
);

// Using the factory with options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('decorrelated-jitter', [
    'baseDelay' => 1.0,
    'maxDelay' => 60,
    'minFactor' => 1.0,
    'maxFactor' => 3.0
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\GuzzleResponseStrategy;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

$strategy = new GuzzleResponseStrategy(
    baseDelay: 1.0,                                   // Base delay in seconds (default: 1.0)
    innerStrategy: new ExponentialBackoffStrategy(baseDelay: 0.5),  // Optional custom inner strategy
    maxDelay: 60
);

// Using the factory with options
// Inner strategy defaults to application default if not specified
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('guzzle-response', [
    'baseDelay' => 1.0, // Configures the default inner strategy's base delay
    'maxDelay' => 60
    // 'innerStrategy' => new CustomInnerStrategy() // Can also provide an instance
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\ResponseContentStrategy;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

$strategy = new ResponseContentStrategy(
    innerStrategy: new ExponentialBackoffStrategy(baseDelay: 0.5),  // Inner strategy controls delay
    retryableContentPatterns: ['/server busy/i', '/try again/i'], // Regex patterns in body
    retryableErrorCodes: ['TRY_AGAIN'],
    errorCodePaths: ['error.status_code']
);

// Using the factory with options
// Options are typically loaded from config('retry.response_content')
// Inner strategy defaults to the application's default retry strategy
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('response-content');

// Or provide specific options (less common, usually configured globally)
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('response-content', [
    'retryableContentPatterns' => ['/custom pattern/'],
    'retryableErrorCodes' => ['MY_CODE'],
    'errorCodePaths' => ['data.error_status'],
    // 'innerStrategy' can also be provided here if needed
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\CircuitBreakerStrategy;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

$strategy = new CircuitBreakerStrategy(
    innerStrategy: new ExponentialBackoffStrategy(baseDelay: 2.0),  // Inner strategy controls delay
    failureThreshold: 3,    // Open after 3 failures
    resetTimeout: 120,      // Stay open for 2 minutes
    cacheKey: 'my_service_circuit', // Optional unique key for this circuit
    cacheTtl: 1440, // Cache TTL in minutes (default: 1 day)
    failOpenOnCacheError: false // Default behavior on cache errors
);

// Using the factory with options
// Creates a circuit breaker using default or named service settings from config
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('circuit-breaker');

// Or with specific options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('circuit-breaker', [
    'failureThreshold' => 5,
    'resetTimeout' => 60,
    // Inner strategy defaults to application default if not specified
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\RateLimitStrategy;
use GregPriday\LaravelRetry\Strategies\FixedDelayStrategy;

$strategy = new RateLimitStrategy(
    innerStrategy: new FixedDelayStrategy(baseDelay: 0.5),  // Inner strategy controls delay
    maxAttempts: 50,    // Max attempts allowed
    timeWindow: 60,     // Within this time window (seconds)
    storageKey: 'api-rate-limiter'  // Unique key for the rate limiter
);

// Using the factory with options
// Inner strategy defaults to application default if not specified
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('rate-limit', [
    'maxAttempts' => 100,
    'timeWindow' => 60,
    'storageKey' => 'my_api_limit'
    // 'innerStrategy' => new CustomInnerStrategy() // Can also provide an instance
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\TotalTimeoutStrategy;
use GregPriday\LaravelRetry\Strategies\LinearBackoffStrategy;

$strategy = new TotalTimeoutStrategy(
    innerStrategy: new LinearBackoffStrategy(baseDelay: 0.5),  // Inner strategy controls delay
    totalTimeout: 30.0    // Complete within 30 seconds (float for precision)
);

// Using the factory with options
// totalTimeout loaded from config('retry.total_timeout')
// Inner strategy defaults to the application's default retry strategy
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('total-timeout');

// Or with specific options
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('total-timeout', [
    'totalTimeout' => 45.5, // Override total timeout
    // 'innerStrategy' => new CustomInnerStrategy() // Can also provide an instance
]);

// Using the class directly
use GregPriday\LaravelRetry\Strategies\CustomOptionsStrategy;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

$strategy = new CustomOptionsStrategy(
    baseDelay: 1.0, // Base delay reference for callbacks
    innerStrategy: new ExponentialBackoffStrategy(baseDelay: 0.5), // Base strategy to potentially fall back to
    options: ['custom_flag' => true, 'user_id' => 123] // Custom data for callbacks
);

// Override the retry decision logic
$strategy->withShouldRetryCallback(function ($attempt, $maxAttempts, $exception, $options) {
    // Only retry if custom_flag is true and within attempts
    return $options['custom_flag'] && $attempt < $maxAttempts;
});

// Override the delay calculation logic
$strategy->withDelayCallback(function ($attempt, $baseDelay, $options) {
    // Use baseDelay, but maybe double it if custom_flag is set
    return $baseDelay * ($options['custom_flag'] ? 2 : 1);
});

// Using the factory with options (callbacks set after instantiation)
$strategy = \GregPriday\LaravelRetry\Factories\StrategyFactory::make('custom-options', [
    'baseDelay' => 2.0,
    'options' => ['initial_option' => 'value']
    // Inner strategy defaults to application default
]);
// $strategy->withShouldRetryCallback(...)
// $strategy->withDelayCallback(...)

// Note: Direct instantiation is generally preferred for this strategy
// since callbacks are fundamental to its functionality

// Using the class directly
use GregPriday\LaravelRetry\Strategies\CallbackRetryStrategy;
use App\Exceptions\CustomTransientException; // Example custom exception

$strategy = new CallbackRetryStrategy(
    // Define how delay is calculated (receives attempt, baseDelay, maxAttempts, exception, options)
    delayCallback: fn($attempt, $baseDelay) => $baseDelay * ($attempt + 1), // e.g., 1s, 2s, 3s...

    // Define when to retry (optional, defaults to checking attempts)
    // Receives (attempt, maxAttempts, exception, options)
    shouldRetryCallback: fn($attempt, $maxAttempts, $exception) =>
        $attempt < $maxAttempts && $exception instanceof CustomTransientException,

    baseDelay: 1.0, // Reference delay value for delayCallback (default: 1.0)
    options: ['log_retries' => true] // Custom data passed to callbacks (default: [])
);

// Usage:
Retry::withStrategy($strategy)->maxRetries(3)->run(fn() => /* operation */);

// Note: Creating via factory alias is less common as callbacks must be provided in the constructor.
// You would typically instantiate this directly as shown above.

use GregPriday\LaravelRetry\Strategies\CircuitBreakerStrategy;
use GregPriday\LaravelRetry\Strategies\RateLimitStrategy;
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy;

// Create a strategy that implements circuit breaking, rate limiting, and exponential backoff
$exponentialStrategy = new ExponentialBackoffStrategy(
    baseDelay: 0.5,
    multiplier: 2.0
);

$rateStrategy = new RateLimitStrategy(
    innerStrategy: $exponentialStrategy,
    maxAttempts: 50,
    timeWindow: 60
);

$strategy = new CircuitBreakerStrategy(
    innerStrategy: $rateStrategy,
    failureThreshold: 3,
    resetTimeout: 120
); 

use GregPriday\LaravelRetry\Facades\Retry;

// Using default exception handlers
$result = Retry::run(function () {
    // This will automatically retry on common HTTP client exceptions
    $response = Http::get('https://api.example.com/data');
    $response->throw();
    return $response->json();
});

use GregPriday\LaravelRetry\Facades\Retry;

$result = Retry::retryIf(function (Throwable $e, int $attempt) {
        // Custom logic to determine if retry is needed
        return $e instanceof CustomException && $attempt < 5;
    })
    ->run(function () {
        // Your operation here
    });

// Or use retryUnless for inverse logic
$result = Retry::retryUnless(function (Throwable $e, int $attempt) {
        return $e instanceof PermanentFailureException;
    })
    ->run(function () {
        // Your operation here
    });

use GregPriday\LaravelRetry\Facades\Retry;
use App\Exceptions\Retry\Handlers\CustomApiHandler;
use Illuminate\Support\Facades\Http;
use Throwable;

// Create a custom handler instance
$handler = new CustomApiHandler();

$result = Retry::retryIf(function (Throwable $e, int $attempt) use ($handler) {
        // First check our custom handler to see if exception is generally retryable
        $handlerAllowsRetry = $handler->isRetryable($e);
        
        // Then add additional specific conditions for this operation
        $isRateLimitError = $e instanceof RequestException && $e->response->status() === 429;
        
        // Only retry rate limit errors for a few attempts
        return $handlerAllowsRetry && (!$isRateLimitError || $attempt < 3);
    })
    ->run(function () {
        $response = Http::get('https://api.example.com/data');
        $response->throw();
        return $response->json();
    });

namespace App\Exceptions\Retry\Handlers;

use GregPriday\LaravelRetry\Contracts\RetryableExceptionHandler;
use GregPriday\LaravelRetry\Exceptions\Handlers\BaseHandler;

class CustomApiHandler extends BaseHandler implements RetryableExceptionHandler
{
    public function isApplicable(): bool
    {
        // Return true if this handler should be active
        return true;
    }

    public function getExceptions(): array
    {
        return [
            CustomApiException::class,
            AnotherCustomException::class,
        ];
    }

    public function getPatterns(): array
    {
        return [
            '/rate limit exceeded/i',
            '/server temporarily unavailable/i',
        ];
    }
}

'handler_paths' => [
    app_path('Exceptions/Retry/Handlers'),
    app_path('Services/API/RetryHandlers'),
    // Add more paths here
],

namespace App\Exceptions\Retry\Handlers;

use GregPriday\LaravelRetry\Exceptions\Handlers\DatabaseHandler;

class CustomDatabaseHandler extends DatabaseHandler 
{
    public function isApplicable(): bool
    {
        // Disable this handler in testing environment
        if (app()->environment('testing')) {
            return false;
        }
        
        return parent::isApplicable();
    }
}

use GregPriday\LaravelRetry\Events\RetryingOperationEvent;
use GregPriday\LaravelRetry\Events\OperationSucceededEvent;
use GregPriday\LaravelRetry\Events\OperationFailedEvent;

protected $listen = [
    RetryingOperationEvent::class => [
        LogRetryAttemptListener::class,
    ],
    OperationSucceededEvent::class => [
        LogSuccessListener::class,
    ],
    OperationFailedEvent::class => [
        LogFailureListener::class,
        NotifyAdminListener::class,
    ],
];

use GregPriday\LaravelRetry\Facades\Retry;
use Illuminate\Support\Facades\Log;

Retry::withEventCallbacks([
    'onRetrying' => function ($event) {
        Log::info('Retrying operation', [
            'operation_id' => $event->context->getOperationId(),
            'attempt' => $event->attemptNumber,
            'delay' => $event->delay,
            'error' => $event->exception->getMessage(),
            'remaining_attempts' => $event->context->getMaxRetries() - $event->attemptNumber + 1,
        ]);
    },
    'onSuccess' => function ($event) {
        Log::info('Operation succeeded', [
            'operation_id' => $event->context->getOperationId(),
            'total_attempts' => $event->context->getTotalAttempts(),
            'total_time' => $event->context->getMetrics()['total_duration'],
            'result' => $event->result,
        ]);
    },
    'onFailure' => function ($event) {
        Log::error('Retry operation failed', [
            'operation_id' => $event->context->getOperationId(),
            'total_attempts' => $event->context->getTotalAttempts(),
            'total_duration' => $event->context->getMetrics()['total_duration'],
            'average_duration' => $event->context->getMetrics()['average_duration'],
            'total_delay' => $event->context->getTotalDelay(),
            'exception' => get_class($event->exception),
            'message' => $event->exception->getMessage(),
            'exception_history' => collect($event->context->getExceptionHistory())
                ->map(fn ($e) => ['class' => get_class($e), 'message' => $e->getMessage()])
                ->toArray(),
            'metadata' => $event->context->getMetadata(),
        ]);
    }
])
->run(function () {
    // Your operation here
});

use GregPriday\LaravelRetry\Events\OperationFailedEvent;
use Illuminate\Support\Facades\Log;

class LogFailureListener
{
    public function handle(OperationFailedEvent $event)
    {
        $context = $event->context;

        Log::error('Retry operation failed', [
            'operation_id' => $context->getOperationId(),
            'total_attempts' => $context->getTotalAttempts(),
            'total_duration' => $context->getMetrics()['total_duration'],
            'average_duration' => $context->getMetrics()['average_duration'],
            'total_delay' => $context->getTotalDelay(),
            'exception' => get_class($event->exception),
            'message' => $event->exception->getMessage(),
            'exception_history' => collect($context->getExceptionHistory())
                ->map(fn ($e) => ['class' => get_class($e), 'message' => $e->getMessage()])
                ->toArray(),
            'metadata' => $context->getMetadata(),
        ]);
    }
}
bash
php artisan vendor:publish --tag="retry-config"
bash
php artisan vendor:publish --tag="retry-handlers"