Как в Guzzle получить финальную ссылку после редиректа

Ещё одна повседневная задача — узнать адрес ссылки на которую был совершён редирект после HTTP-запроса. Однако, при работе с библиотекой Guzzle делается это не очень очевидно. Во-первых, нужно разрешить клиенту совершать редиректы: ‘allow_redirects’ => [ ‘max’ => 5, ‘strict’ => true]. Во-вторых, нужно включить запись редиректов: ‘allow_redirects’ => [ ‘track_redirects’ => true ]. В третьи, нужно прокинуть анонимную функцию с замыканием, которая запишет значение «эффективного» URL в замкнутую переменную. Всё вместе выглядит так:

$options = ['allow_redirects' => [
  'track_redirects' => true,
  'max' => 5,
  'strict' => true,
  ],
 'on_stats' => function(TransferStats $stats) use (&$final) {
  $final = $stats->getEffectiveUri();
}];
$res = $this->client->get($url, $options);
$response = $res->getBody()->getContents();

Финальная ссылка будет в переменной $final. Невероятно, но факт!

Способ через RedirectMiddleware

Есть ещё один способ, который использует идущую в комплекте с Guzzle RedirectMiddleware. Он возвращает массив со всеми совершёнными редиректами:

$client = new \GuzzleHttp\Client(['allow_redirects' => ['track_redirects' => true]]);
$response = $client->request('GET', $url);
var_dump($response->getHeader(\GuzzleHttp\RedirectMiddleware::HISTORY_HEADER));

При включенном track_redirects в ответ добавляются служебные заголовки с названием \GuzzleHttp\RedirectMiddleware::HISTORY_HEADER является X-Guzzle-Redirect-History. Однако, есть один нюанс, в массиве окажется только история редиректов. То есть, если не было совершено ни одного редиректа — массив будет пустым. В данном случае финальной ссылкой нужно считать ту, которая изначально и была запрошена.

Кстати, свойство track_redirects можно изменять динамически:

\GuzzleHttp\RedirectMiddleware::$defaultSettings['track_redirects'] = true;

Вместо метода $response->getHeader() можно использовать метод getHeaderLine(), в таком случае заголовок вернётся не в виде массива, а в виде строки, где значения разделены запятой. Этот вариант менее предпочтителен, т.к. не все значения корректно можно будет расклеить обратно.

Использование Guzzle Middleware

А есть ещё какие-нибудь варианты? Да! Как минимум есть ещё 3 способа сделать это. И оба связаны с использованием Middleware. Для начала нужно подготовиться:

$stack = \GuzzleHttp\HandlerStack::create();
$client = new Client([
    'handler' => $stack,
    \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => true
]);

Способ с замыканием

Теперь в экземпляр HandlerStack можно добавлять посредника, который будет получать финальную ссылку. Рассмотрим простой и не красивый вариант:

$lastRequest = null;
$stack->push(\GuzzleHttp\Middleware::mapRequest(function (\Psr\Http\Message\RequestInterface $request) use(&$lastRequest) {
    return $lastRequest = $request;
}));
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org/redirect-to?url=http://stackoverflow.com');
$response = $client->send($request);
var_dump($lastRequest->getUri()->__toString());

Собственно $stack->push добавляет обработчик к Guzzle-клиенту. В данном случае в качестве обработчика используется анонимная функция с замыканием. После выполнения запроса в переменной $lastRequest окажется объект запроса из которого можно извлечь финальную ссылку после редиректов. Окей, это работает. Но с замыканием это выглядит грязно и некрасиво. Можно это улучшить?

Способ без замыкания

Давайте создадим такой класс:

class EffectiveUrlMiddleware {
    /**
     * @var \Psr\Http\Message\RequestInterface
     */
    private $lastRequest;
    /**
     * @param \Psr\Http\Message\RequestInterface $request
     *
     * @return \Psr\Http\Message\RequestInterface
     */
    public function __invoke(\Psr\Http\Message\RequestInterface $request) {
        return $this->lastRequest = $request;
    }
    /**
     * @return \Psr\Http\Message\RequestInterface
     */
    public function getLastRequest() {
        return $this->lastRequest;
    }
    /**
     * @return string
     */
    public function getFinalUrl() {
        return $this->lastRequest->getUri()->__toString();
    }
}

И теперь можно прокинуть в HandlerStack объект этого класса:

$EffectiveUrlMiddleware = new EffectiveUrlMiddleware();
$stack->push(\GuzzleHttp\Middleware::mapRequest($EffectiveUrlMiddleware));

А финальную ссылку можно будет достать так:

var_dump($EffectiveUrlMiddleware->getFinalUrl());

Но это опять же не самый красивый вариант, так как нужно иметь доступ к объекту $EffectiveUrlMiddleware для получения финальной ссылки.

Способ с заголовком ответа

На самом деле этот вариант безнадёжно устарел и идентичен варианту с использованием дефолтного RedirectMiddleware и заголовка X-Guzzle-Redirect-History. Для начала нужно модифицировать класс:

class EffectiveUrlMiddleware {
    /**
     * @var Callable
     */
    protected $nextHandler;
    /**
     * @var string
     */
    protected $headerName;
    /**
     * @param callable $nextHandler
     * @param string   $headerName  The header name to use for storing effective url
     */
    public function __construct(callable $nextHandler, $headerName = 'X-GUZZLE-EFFECTIVE-URL') {
        $this->nextHandler = $nextHandler;
        $this->headerName = $headerName;
    }
    /**
     * Inject effective-url header into response.
     *
     * @param \Psr\Http\Message\RequestInterface $request
     * @param array            $options
     *
     * @return \Psr\Http\Message\RequestInterface
     */
    public function __invoke(\Psr\Http\Message\RequestInterface $request, array $options) {
        $fn = $this->nextHandler;
        return $fn($request, $options)->then(function (\Psr\Http\Message\ResponseInterface $response) use ($request, $options) {
            return $response->withHeader($this->headerName, $request->getUri()->__toString());
        });
    }
    /**
     * Prepare a middleware closure to be used with HandlerStack
     *
     * @param string $headerName The header name to use for storing effective url
     *
     * @return \Closure
     */
    public static function middleware($headerName = 'X-GUZZLE-EFFECTIVE-URL') {
        return function (callable $handler) use (&$headerName) {
            return new static($handler, $headerName);
        };
    }
}

Добавление обработчика:

$stack()->push(EffectiveUrlMiddleware::middleware());

И после выполнения запроса:

echo $response->getHeaderLine('X-GUZZLE-EFFECTIVE-URL');