1<?php
2namespace GuzzleHttp;
3
4use GuzzleHttp\Promise\PromiseInterface;
5use GuzzleHttp\Promise\RejectedPromise;
6use GuzzleHttp\Psr7;
7use Psr\Http\Message\RequestInterface;
8use Psr\Http\Message\ResponseInterface;
9
10/**
11 * Middleware that retries requests based on the boolean result of
12 * invoking the provided "decider" function.
13 */
14class RetryMiddleware
15{
16    /** @var callable  */
17    private $nextHandler;
18
19    /** @var callable */
20    private $decider;
21
22    /** @var callable */
23    private $delay;
24
25    /**
26     * @param callable $decider     Function that accepts the number of retries,
27     *                              a request, [response], and [exception] and
28     *                              returns true if the request is to be
29     *                              retried.
30     * @param callable $nextHandler Next handler to invoke.
31     * @param callable $delay       Function that accepts the number of retries
32     *                              and [response] and returns the number of
33     *                              milliseconds to delay.
34     */
35    public function __construct(
36        callable $decider,
37        callable $nextHandler,
38        callable $delay = null
39    ) {
40        $this->decider = $decider;
41        $this->nextHandler = $nextHandler;
42        $this->delay = $delay ?: __CLASS__ . '::exponentialDelay';
43    }
44
45    /**
46     * Default exponential backoff delay function.
47     *
48     * @param int $retries
49     *
50     * @return int milliseconds.
51     */
52    public static function exponentialDelay($retries)
53    {
54        return (int) pow(2, $retries - 1) * 1000;
55    }
56
57    /**
58     * @param RequestInterface $request
59     * @param array            $options
60     *
61     * @return PromiseInterface
62     */
63    public function __invoke(RequestInterface $request, array $options)
64    {
65        if (!isset($options['retries'])) {
66            $options['retries'] = 0;
67        }
68
69        $fn = $this->nextHandler;
70        return $fn($request, $options)
71            ->then(
72                $this->onFulfilled($request, $options),
73                $this->onRejected($request, $options)
74            );
75    }
76
77    /**
78     * Execute fulfilled closure
79     *
80     * @return mixed
81     */
82    private function onFulfilled(RequestInterface $req, array $options)
83    {
84        return function ($value) use ($req, $options) {
85            if (!call_user_func(
86                $this->decider,
87                $options['retries'],
88                $req,
89                $value,
90                null
91            )) {
92                return $value;
93            }
94            return $this->doRetry($req, $options, $value);
95        };
96    }
97
98    /**
99     * Execute rejected closure
100     *
101     * @return callable
102     */
103    private function onRejected(RequestInterface $req, array $options)
104    {
105        return function ($reason) use ($req, $options) {
106            if (!call_user_func(
107                $this->decider,
108                $options['retries'],
109                $req,
110                null,
111                $reason
112            )) {
113                return \GuzzleHttp\Promise\rejection_for($reason);
114            }
115            return $this->doRetry($req, $options);
116        };
117    }
118
119    /**
120     * @return self
121     */
122    private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null)
123    {
124        $options['delay'] = call_user_func($this->delay, ++$options['retries'], $response);
125
126        return $this($request, $options);
127    }
128}
129