1<?php
2
3namespace GuzzleHttp;
4
5use GuzzleHttp\Cookie\CookieJarInterface;
6use GuzzleHttp\Exception\RequestException;
7use GuzzleHttp\Promise as P;
8use GuzzleHttp\Promise\PromiseInterface;
9use Psr\Http\Message\RequestInterface;
10use Psr\Http\Message\ResponseInterface;
11use Psr\Log\LoggerInterface;
12
13/**
14 * Functions used to create and wrap handlers with handler middleware.
15 */
16final class Middleware
17{
18    /**
19     * Middleware that adds cookies to requests.
20     *
21     * The options array must be set to a CookieJarInterface in order to use
22     * cookies. This is typically handled for you by a client.
23     *
24     * @return callable Returns a function that accepts the next handler.
25     */
26    public static function cookies(): callable
27    {
28        return static function (callable $handler): callable {
29            return static function ($request, array $options) use ($handler) {
30                if (empty($options['cookies'])) {
31                    return $handler($request, $options);
32                } elseif (!($options['cookies'] instanceof CookieJarInterface)) {
33                    throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface');
34                }
35                $cookieJar = $options['cookies'];
36                $request = $cookieJar->withCookieHeader($request);
37
38                return $handler($request, $options)
39                    ->then(
40                        static function (ResponseInterface $response) use ($cookieJar, $request): ResponseInterface {
41                            $cookieJar->extractCookies($request, $response);
42
43                            return $response;
44                        }
45                    );
46            };
47        };
48    }
49
50    /**
51     * Middleware that throws exceptions for 4xx or 5xx responses when the
52     * "http_errors" request option is set to true.
53     *
54     * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages.
55     *
56     * @return callable(callable): callable Returns a function that accepts the next handler.
57     */
58    public static function httpErrors(BodySummarizerInterface $bodySummarizer = null): callable
59    {
60        return static function (callable $handler) use ($bodySummarizer): callable {
61            return static function ($request, array $options) use ($handler, $bodySummarizer) {
62                if (empty($options['http_errors'])) {
63                    return $handler($request, $options);
64                }
65
66                return $handler($request, $options)->then(
67                    static function (ResponseInterface $response) use ($request, $bodySummarizer) {
68                        $code = $response->getStatusCode();
69                        if ($code < 400) {
70                            return $response;
71                        }
72                        throw RequestException::create($request, $response, null, [], $bodySummarizer);
73                    }
74                );
75            };
76        };
77    }
78
79    /**
80     * Middleware that pushes history data to an ArrayAccess container.
81     *
82     * @param array|\ArrayAccess<int, array> $container Container to hold the history (by reference).
83     *
84     * @return callable(callable): callable Returns a function that accepts the next handler.
85     *
86     * @throws \InvalidArgumentException if container is not an array or ArrayAccess.
87     */
88    public static function history(&$container): callable
89    {
90        if (!\is_array($container) && !$container instanceof \ArrayAccess) {
91            throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess');
92        }
93
94        return static function (callable $handler) use (&$container): callable {
95            return static function (RequestInterface $request, array $options) use ($handler, &$container) {
96                return $handler($request, $options)->then(
97                    static function ($value) use ($request, &$container, $options) {
98                        $container[] = [
99                            'request' => $request,
100                            'response' => $value,
101                            'error' => null,
102                            'options' => $options,
103                        ];
104
105                        return $value;
106                    },
107                    static function ($reason) use ($request, &$container, $options) {
108                        $container[] = [
109                            'request' => $request,
110                            'response' => null,
111                            'error' => $reason,
112                            'options' => $options,
113                        ];
114
115                        return P\Create::rejectionFor($reason);
116                    }
117                );
118            };
119        };
120    }
121
122    /**
123     * Middleware that invokes a callback before and after sending a request.
124     *
125     * The provided listener cannot modify or alter the response. It simply
126     * "taps" into the chain to be notified before returning the promise. The
127     * before listener accepts a request and options array, and the after
128     * listener accepts a request, options array, and response promise.
129     *
130     * @param callable $before Function to invoke before forwarding the request.
131     * @param callable $after  Function invoked after forwarding.
132     *
133     * @return callable Returns a function that accepts the next handler.
134     */
135    public static function tap(callable $before = null, callable $after = null): callable
136    {
137        return static function (callable $handler) use ($before, $after): callable {
138            return static function (RequestInterface $request, array $options) use ($handler, $before, $after) {
139                if ($before) {
140                    $before($request, $options);
141                }
142                $response = $handler($request, $options);
143                if ($after) {
144                    $after($request, $options, $response);
145                }
146
147                return $response;
148            };
149        };
150    }
151
152    /**
153     * Middleware that handles request redirects.
154     *
155     * @return callable Returns a function that accepts the next handler.
156     */
157    public static function redirect(): callable
158    {
159        return static function (callable $handler): RedirectMiddleware {
160            return new RedirectMiddleware($handler);
161        };
162    }
163
164    /**
165     * Middleware that retries requests based on the boolean result of
166     * invoking the provided "decider" function.
167     *
168     * If no delay function is provided, a simple implementation of exponential
169     * backoff will be utilized.
170     *
171     * @param callable $decider Function that accepts the number of retries,
172     *                          a request, [response], and [exception] and
173     *                          returns true if the request is to be retried.
174     * @param callable $delay   Function that accepts the number of retries and
175     *                          returns the number of milliseconds to delay.
176     *
177     * @return callable Returns a function that accepts the next handler.
178     */
179    public static function retry(callable $decider, callable $delay = null): callable
180    {
181        return static function (callable $handler) use ($decider, $delay): RetryMiddleware {
182            return new RetryMiddleware($decider, $handler, $delay);
183        };
184    }
185
186    /**
187     * Middleware that logs requests, responses, and errors using a message
188     * formatter.
189     *
190     * @phpstan-param \Psr\Log\LogLevel::* $logLevel  Level at which to log requests.
191     *
192     * @param LoggerInterface                            $logger    Logs messages.
193     * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings.
194     * @param string                                     $logLevel  Level at which to log requests.
195     *
196     * @return callable Returns a function that accepts the next handler.
197     */
198    public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable
199    {
200        // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter
201        if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) {
202            throw new \LogicException(sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class));
203        }
204
205        return static function (callable $handler) use ($logger, $formatter, $logLevel): callable {
206            return static function (RequestInterface $request, array $options = []) use ($handler, $logger, $formatter, $logLevel) {
207                return $handler($request, $options)->then(
208                    static function ($response) use ($logger, $request, $formatter, $logLevel): ResponseInterface {
209                        $message = $formatter->format($request, $response);
210                        $logger->log($logLevel, $message);
211
212                        return $response;
213                    },
214                    static function ($reason) use ($logger, $request, $formatter): PromiseInterface {
215                        $response = $reason instanceof RequestException ? $reason->getResponse() : null;
216                        $message = $formatter->format($request, $response, P\Create::exceptionFor($reason));
217                        $logger->error($message);
218
219                        return P\Create::rejectionFor($reason);
220                    }
221                );
222            };
223        };
224    }
225
226    /**
227     * This middleware adds a default content-type if possible, a default
228     * content-length or transfer-encoding header, and the expect header.
229     */
230    public static function prepareBody(): callable
231    {
232        return static function (callable $handler): PrepareBodyMiddleware {
233            return new PrepareBodyMiddleware($handler);
234        };
235    }
236
237    /**
238     * Middleware that applies a map function to the request before passing to
239     * the next handler.
240     *
241     * @param callable $fn Function that accepts a RequestInterface and returns
242     *                     a RequestInterface.
243     */
244    public static function mapRequest(callable $fn): callable
245    {
246        return static function (callable $handler) use ($fn): callable {
247            return static function (RequestInterface $request, array $options) use ($handler, $fn) {
248                return $handler($fn($request), $options);
249            };
250        };
251    }
252
253    /**
254     * Middleware that applies a map function to the resolved promise's
255     * response.
256     *
257     * @param callable $fn Function that accepts a ResponseInterface and
258     *                     returns a ResponseInterface.
259     */
260    public static function mapResponse(callable $fn): callable
261    {
262        return static function (callable $handler) use ($fn): callable {
263            return static function (RequestInterface $request, array $options) use ($handler, $fn) {
264                return $handler($request, $options)->then($fn);
265            };
266        };
267    }
268}
269