Рано или поздно любой проект на Yii2 начинает общаться с внешним миром: отправлять данные в CRM, получать курсы валют, дергать API платежных систем. И тут на сцену выходит Guzzle — мощный HTTP-клиент, который уже встроен в Yii2 по умолчанию. В этой статье разберем, как подружить ваш проект с чужими сервисами без боли и лишних костылей.

Digital-студия WNDER

Почему Guzzle, а не file_get_contents?

Многие начинающие разработчики используют file_get_contents() для запросов к API. Работает? Да. Но только пока запрос простой и сервер отвечает мгновенно. Стоит добавить заголовки, авторизацию или обработать ошибку — и начинается боль. Guzzle решает эти проблемы из коробки.

Вот что вы получаете с Guzzle:

  • Удобная работа с заголовками, куками и авторизацией.
  • Автоматическая обработка редиректов.
  • Поддержка асинхронных запросов.
  • Гибкая настройка таймаутов и повторных попыток.
  • Понятные исключения при ошибках.

Yii2 уже включает Guzzle в зависимости, так что отдельно устанавливать его не нужно. Достаточно убедиться, что пакет guzzlehttp/guzzle есть в composer.json.

Базовый запрос: получаем данные из API

Начнем с простого. Допустим, нам нужно получить список пользователей с внешнего сервиса. Создадим компонент, который будет отвечать за интеграцию.

client = new Client([
            'base_uri' => $this->baseUrl,
            'timeout'  => $this->timeout,
            'headers'  => [
                'Accept' => 'application/json',
            ],
        ]);
    }

    public function getUsers()
    {
        try {
            $response = $this->client->get('/users');
            $data = json_decode($response->getBody()->getContents(), true);
            return $data;
        } catch (GuzzleException $e) {
            Yii::error('API error: ' . $e->getMessage());
            return null;
        }
    }
}
?>

Теперь достаточно зарегистрировать компонент в конфиге приложения и вызывать его методы из любого места.

// config/web.php
'components' => [
    'externalApi' => [
        'class' => 'app\components\ExternalApi',
        'baseUrl' => 'https://api.example.com',
    ],
],

Использование в контроллере:

$users = Yii::$app->externalApi->getUsers();
if ($users === null) {
    return $this->render('error', ['message' => 'Сервис недоступен']);
}
return $this->render('users', ['users' => $users]);
Правило: Никогда не делайте HTTP-запросы прямо в контроллере или модели. Выносите интеграцию в отдельный компонент или сервис — так код проще тестировать и переиспользовать.

Отправляем данные: POST-запросы и авторизация

Часто API требует не только получать, но и отправлять данные. Например, создать заказ или обновить профиль. Guzzle позволяет легко отправлять JSON и добавлять токены авторизации.

public function createOrder(array $orderData)
{
    try {
        $response = $this->client->post('/orders', [
            'json' => $orderData,
            'headers' => [
                'Authorization' => 'Bearer ' . $this->getToken(),
                'Content-Type'  => 'application/json',
            ],
        ]);

        return json_decode($response->getBody()->getContents(), true);
    } catch (GuzzleException $e) {
        Yii::error('Order creation failed: ' . $e->getMessage());
        throw new \Exception('Не удалось создать заказ');
    }
}

private function getToken()
{
    // Здесь можно кешировать токен, чтобы не запрашивать каждый раз
    return Yii::$app->cache->getOrSet('api_token', function () {
        $response = $this->client->post('/auth', [
            'json' => [
                'login' => Yii::$app->params['apiLogin'],
                'password' => Yii::$app->params['apiPassword'],
            ],
        ]);
        $data = json_decode($response->getBody()->getContents(), true);
        return $data['token'];
    }, 3600);
}

Обратите внимание на кеширование токена. Если API выдает токен на час, нет смысла запрашивать его перед каждым запросом. Yii::cache с методом getOrSet отлично с этим справляется.

Обработка ошибок и таймауты

Внешний сервис может упасть, зависнуть или вернуть ошибку 500. Ваша задача — не дать приложению упасть вместе с ним. Guzzle выбрасывает исключения, которые нужно правильно обрабатывать.

Вот основные типы исключений:

Исключение Когда возникает Что делать
ConnectException Сервер недоступен Повторить запрос позже, залогировать
RequestException Ошибка 4xx или 5xx Разобрать тело ответа, показать пользователю
TooManyRedirectsException Слишком много редиректов Проверить настройки, ограничить количество
ServerException Ошибка 5xx Повторить запрос, уведомить администратора

Чтобы не дублировать код обработки, можно создать middleware или обертку. Но самый простой способ — использовать метод request() с параметром http_errors => false, чтобы Guzzle не выбрасывал исключения на 4xx и 5xx, а возвращал ответ как есть.

$response = $this->client->request('GET', '/users', [
    'http_errors' => false,
]);

if ($response->getStatusCode() >= 400) {
    Yii::error('API returned error: ' . $response->getBody());
    return null;
}

return json_decode($response->getBody()->getContents(), true);

Такой подход дает больше контроля. Вы сами решаете, как реагировать на ошибки.

Лайфхак: Установите таймаут не больше 10 секунд. Если внешний сервис не отвечает за это время, скорее всего, он не ответит вообще. Лучше быстро вернуть ошибку, чем держать пользователя в ожидании.

Повторные запросы и асинхронность

Иногда сервис отвечает с ошибкой из-за временных проблем. В таких случаях помогает повторный запрос. Guzzle поддерживает middleware для retry.

use GuzzleHttp\Middleware;
use GuzzleHttp\HandlerStack;

$handlerStack = HandlerStack::create();
$handlerStack->push(Middleware::retry(
    function ($retries, $request, $response, $exception) {
        // Повторяем, если ошибка 500 или таймаут, но не больше 3 раз
        if ($retries >= 3) {
            return false;
        }
        if ($exception instanceof ConnectException) {
            return true;
        }
        if ($response && $response->getStatusCode() >= 500) {
            return true;
        }
        return false;
    },
    function ($retries) {
        // Задержка перед повтором: 100ms, 200ms, 400ms
        return 100 * (2 ** $retries);
    }
));

$client = new Client([
    'handler' => $handlerStack,
    'base_uri' => 'https://api.example.com',
]);

Асинхронные запросы позволяют отправлять несколько запросов одновременно и не ждать каждого по отдельности. Это особенно полезно, когда нужно собрать данные из нескольких источников.

use GuzzleHttp\Promise;

$promises = [
    'users' => $client->getAsync('/users'),
    'orders' => $client->getAsync('/orders'),
];

$results = Promise\Utils::settle($promises)->wait();

foreach ($results as $key => $result) {
    if ($result['state'] === 'fulfilled') {
        $data[$key] = json_decode($result['value']->getBody(), true);
    } else {
        Yii::error("Failed to fetch $key: " . $result['reason']);
    }
}

Такой подход экономит время: вместо последовательных запросов вы делаете их параллельно.

Что в итоге

Guzzle — это не просто HTTP-клиент, а полноценный инструмент для интеграции с внешними сервисами. В Yii2 он уже есть, так что не нужно изобретать велосипед. Выносите логику в компоненты, кешируйте токены, обрабатывайте ошибки и не забывайте про таймауты. Тогда ваш проект будет стабильно работать даже с самыми капризными API.

Главное правило: не доверяйте внешнему сервису. Он может упасть, ответить медленно или вернуть неожиданные данные. Ваша задача — сделать так, чтобы пользователь этого не заметил.

Студия WNDER