23 September 2021

Introducing PHP Retrier

Summary
PHP Retrier is a composer package I created to use in our projects. There were many times that we needed to add retry logic to a particular code. This package allows developers to so easily in a customizable fashion.

I've just deployed a new composer library that you can use to retry your logic like this:

<?php

$result = (new Retrier())
    // Default delay is 3 seconds, default retry times is 3 times
    ->setLogic(function() use($api) {
        return $api->get();
    })
    ->execute();

The full readme is at https://github.com/s-patompong/php-retrier#readme. This post shows the example use cases that I use in my project.

Normally, I use Guzzle to make an API call. Let's try adding the Retrier class when making the call, so if the GuzzleHttp\Exception\ClientException got thrown from Guzzle, we will retry five times and wait 10 seconds before retrying the code.

Let's start with creating our own RetryStrategy so we can capture Guzzle's ClientException.

<?php

namespace Pond\PhpRetrierTest;

use GuzzleHttp\Exception\ClientException;
use SPatompong\Retrier\Contracts\RetryStrategy;

class RetryGuzzleClientExceptionStrategy implements RetryStrategy
{
    public function shouldRetry(mixed $value): bool{
        return $value instanceof ClientException;
    }
}

Now, we create a script to fetch some data from the API, let's say Github API, for simplicity.

<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Pond\PhpRetrierTest\RetryGuzzleClientExceptionStrategy;
use SPatompong\Retrier\Retrier;

require __DIR__ . "/vendor/autoload.php";

$retrier = (new Retrier())
    // Set the retry logic
    ->setRetryStrategy(new RetryGuzzleClientExceptionStrategy())

    // Set retry times and delay
    ->setRetryTimes(5)
    ->setDelay(10)

    // Set the actual logic
    ->setLogic(fn() => (new Client())->request('GET', 'https://api.github.com/repos/guzzle/guzzle'))

    // We can listen to the retry event and act appropriately
    ->setOnRetryListener(function ($try, $value, $throwable) {
        echo "Failed to get data, retried: $try times.\n";
    });

// We still need to do try/catch because even after 5 tries, it's possible that we still get an exception
try {
    $response = $retrier->execute();
    echo "Get response success!";
} catch (ClientException $exception) {
    echo "After 5 retries we still get ClientException\n";
}

If the API is working ok and we get the response from the first try, running this script would yield this output.

Get response success!

However, if somehow the API doesn't work (GitHub has a power outage problem, for example). We will get this output.

Failed to get data, retried: 1 times. Wait 10 seconds before try again.
Failed to get data, retried: 2 times. Wait 10 seconds before try again.
Failed to get data, retried: 3 times. Wait 10 seconds before try again.
Failed to get data, retried: 4 times. Wait 10 seconds before try again.
Failed to get data, retried: 5 times. Wait 10 seconds before try again.
After 5 retries we still get ClientException