Ещё одна повседневная задача — узнать адрес ссылки на которую был совершён редирект после 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');