GuzzleHTTP/Promises для PHP: полное руководство с примерами
Добро пожаловать в подробное руководство по библиотеке GuzzleHTTP/Promises для языка PHP. Эта библиотека является одной из ключевых в экосистеме Guzzle и предоставляет мощную реализацию паттерна "Promise" (Обещание) для асинхронного программирования. Если вы работаете с HTTP-запросами, параллельными задачами или просто хотите сделать ваш PHP-код более эффективным и читаемым — эта статья для вас.
1. Что это и зачем нужна
GuzzleHTTP/Promises — это PHP-библиотека, реализующая концепцию Promises (промисов) и асинхронного выполнения задач. Она является частью экосистемы Guzzle, но может использоваться полностью независимо от HTTP-клиента. Основная идея промисов заключается в том, что вы создаете объект, который представляет собой результат асинхронной операции, который может быть получен сейчас, позже или никогда. Это позволяет писать код, который не блокирует выполнение программы в ожидании завершения длительных операций.
Зачем это нужно в PHP? Традиционно PHP является синхронным языком, где каждая операция выполняется последовательно. Однако с ростом популярности микросервисов, API-интеграций и работы с внешними сервисами, возникла необходимость в эффективном управлении параллельными задачами. GuzzleHTTP/Promises решает эту проблему, позволяя запускать несколько операций одновременно (например, несколько HTTP-запросов к разным API) и обрабатывать их результаты по мере завершения, не дожидаясь выполнения всех по очереди.
Библиотека предоставляет полный набор инструментов для работы с асинхронным кодом: создание промисов, их объединение, цепочки вызовов, обработку ошибок и управление временем ожидания. Она является стандартом де-факто для асинхронной работы в PHP-сообществе и используется в таких популярных проектах, как Laravel, Symfony, WordPress (через плагины) и, конечно, сам Guzzle HTTP-клиент.
2. Установка
Установка библиотеки производится через Composer — стандартный менеджер пакетов для PHP. Выполните следующую команду в терминале в корне вашего проекта:
composer require guzzlehttp/promises
Если вы используете Guzzle HTTP-клиент, библиотека промисов уже включена в его зависимости, и отдельная установка не требуется. После установки убедитесь, что файл vendor/autoload.php подключен в вашем скрипте:
require 'vendor/autoload.php';
3. Быстрый старт — минимальный рабочий пример
Давайте создадим простейший пример работы с промисами. Мы создадим промис, который "выполняется" через 1 секунду и возвращает строку "Hello, World!":
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\Utils;
// Создаем новый промис
$promise = new Promise(function () use (&$promise) {
// Симуляция асинхронной операции
usleep(1000000); // 1 секунда
$promise->resolve('Hello, World!');
});
// Добавляем обработчик для успешного выполнения
$promise->then(function ($value) {
echo "Получено: " . $value . PHP_EOL;
});
// Запускаем выполнение промиса
Utils::queue()->add(function () use ($promise) {
$promise->wait();
});
// Запускаем очередь
Utils::queue()->run();
Этот пример демонстрирует базовую механику: создание промиса, его разрешение (resolve) и обработку результата через метод then.
4. Основные методы/функции/классы
Рассмотрим 8+ ключевых компонентов библиотеки с подробным описанием и примерами.
4.1. Класс GuzzleHttp\Promise\Promise
Сигнатура: new Promise(callable $waitFn = null, callable $canceller = null)
Что делает: Создает новый промис. Опционально принимает функцию ожидания ($waitFn), которая вызывается при вызове метода wait(), и функцию отмены ($canceller), вызываемую при отмене промиса.
use GuzzleHttp\Promise\Promise;
$promise = new Promise(function () use (&$promise) {
// Асинхронная работа
$result = file_get_contents('https://api.example.com/data');
$promise->resolve($result);
});
$promise->then(function ($data) {
echo "Данные получены: " . substr($data, 0, 100) . "...\n";
});
4.2. Метод then()
Сигнатура: $promise->then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
Что делает: Добавляет обработчики для успешного выполнения ($onFulfilled) и для ошибки ($onRejected). Возвращает новый промис, позволяя строить цепочки.
$promise = new Promise(function () use (&$promise) {
$promise->resolve(42);
});
$promise
->then(function ($value) {
return $value * 2;
})
->then(function ($value) {
echo "Результат: " . $value . PHP_EOL; // Выведет: Результат: 84
});
4.3. Метод otherwise()
Сигнатура: $promise->otherwise(callable $onRejected): PromiseInterface
Что делает: Специализированный метод для обработки только ошибок. Эквивалентен вызову then(null, $onRejected).
$promise = new Promise(function () use (&$promise) {
$promise->reject(new \RuntimeException('Что-то пошло не так'));
});
$promise->otherwise(function (\Throwable $e) {
echo "Ошибка: " . $e->getMessage() . PHP_EOL;
});
4.4. Функция GuzzleHttp\Promise\Utils::queue()
Сигнатура: Utils::queue(): TaskQueueInterface
Что делает: Возвращает глобальную очередь задач, которая управляет выполнением промисов. Позволяет добавлять задачи и запускать их выполнение.
use GuzzleHttp\Promise\Utils;
$queue = Utils::queue();
$queue->add(function () {
echo "Задача 1 выполнена\n";
});
$queue->add(function () {
echo "Задача 2 выполнена\n";
});
$queue->run(); // Выполнит обе задачи
4.5. Функция GuzzleHttp\Promise\Utils::all()
Сигнатура: Utils::all(array $promises): PromiseInterface
Что делает: Принимает массив промисов и возвращает новый промис, который разрешается, когда все переданные промисы будут выполнены. Если хотя бы один промис завершится ошибкой, результирующий промис также будет отклонен.
use GuzzleHttp\Promise\Utils;
use GuzzleHttp\Promise\Promise;
$promises = [
'first' => new Promise(function (&$resolve) { $resolve('Результат 1'); }),
'second' => new Promise(function (&$resolve) { $resolve('Результат 2'); }),
];
$allPromise = Utils::all($promises);
$allPromise->then(function (array $results) {
print_r($results);
});
4.6. Функция GuzzleHttp\Promise\Utils::settle()
Сигнатура: Utils::settle(array $promises): PromiseInterface
Что делает: Аналогична all(), но ожидает завершения всех промисов независимо от того, были они выполнены или отклонены. Возвращает массив с состоянием каждого промиса.
$promises = [
new Promise(function (&$resolve) { $resolve('OK'); }),
new Promise(function (&$reject) { $reject(new \Exception('Ошибка')); }),
];
Utils::settle($promises)->then(function (array $results) {
foreach ($results as $result) {
echo "Состояние: " . $result['state'] . PHP_EOL;
if ($result['state'] === 'fulfilled') {
echo "Значение: " . $result['value'] . PHP_EOL;
} else {
echo "Причина: " . $result['reason']->getMessage() . PHP_EOL;
}
}
});
4.7. Функция GuzzleHttp\Promise\Utils::unwrap()
Сигнатура: Utils::unwrap(array $promises): array
Что делает: Синхронно ожидает выполнения всех промисов и возвращает массив результатов. Если какой-либо промис отклонен, выбрасывается исключение.
$promises = [
new Promise(function (&$resolve) { $resolve(1); }),
new Promise(function (&$resolve) { $resolve(2); }),
];
try {
$results = Utils::unwrap($promises);
print_r($results); // [1, 2]
} catch (\Throwable $e) {
echo "Ошибка: " . $e->getMessage();
}
4.8. Функция GuzzleHttp\Promise\Utils::inspect()
Сигнатура: Utils::inspect(PromiseInterface $promise): array
Что делает: Позволяет "заглянуть" в состояние промиса без ожидания его выполнения. Возвращает массив с ключами state, value (если выполнен) или reason (если отклонен).
$promise = new Promise(function (&$resolve) { $resolve('Готово'); });
$state = Utils::inspect($promise);
echo "Состояние: " . $state['state'] . PHP_EOL; // pending (ожидание)
// После выполнения
$promise->wait();
$state = Utils::inspect($promise);
echo "Состояние: " . $state['state'] . PHP_EOL; // fulfilled
echo "Значение: " . $state['value'] . PHP_EOL; // Готово
4.9. Функция GuzzleHttp\Promise\Utils::task()
Сигнатура: Utils::task(callable $handler): PromiseInterface
Что делает: Создает промис, который выполняется асинхронно через очередь задач. Удобно для оборачивания синхронных функций в асинхронный контекст.
$promise = Utils::task(function () {
// Тяжелая работа
sleep(2);
return 'Данные обработаны';
});
$promise->then(function ($result) {
echo $result . PHP_EOL;
});
Utils::queue()->run();
4.10. Функция GuzzleHttp\Promise\Utils::isFulfilled()
Сигнатура: Utils::isFulfilled(PromiseInterface $promise): bool
Что делает: Проверяет, выполнен ли промис успешно.
$promise = new Promise(function (&$resolve) { $resolve('OK'); });
echo "Выполнен? " . (Utils::isFulfilled($promise) ? 'Да' : 'Нет') . PHP_EOL; // Нет
$promise->wait();
echo "Выполнен? " . (Utils::isFulfilled($promise) ? 'Да' : 'Нет') . PHP_EOL; // Да
5. Реальный практический пример
Рассмотрим реальный сценарий: нам нужно получить данные о погоде для трех городов из внешнего API, обработать их и сохранить в файл. Используем Guzzle HTTP-клиент вместе с промисами для параллельного выполнения запросов.
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Promise\Utils;
use GuzzleHttp\Promise\Promise;
class WeatherAggregator
{
private Client $httpClient;
private array $cities;
private string $apiKey;
public function __construct(string $apiKey, array $cities)
{
$this->apiKey = $apiKey;
$this->cities = $cities;
$this->httpClient = new Client([
'base_uri' => 'https://api.openweathermap.org/data/2.5/',
'timeout' => 10.0,
]);
}
/**
* Создает промис для получения погоды в одном городе
*/
private function fetchWeatherForCity(string $city): Promise
{
$promise = new Promise(function () use ($city, &$promise) {
try {
$response = $this->httpClient->get('weather', [
'query' => [
'q' => $city,
'appid' => $this->apiKey,
'units' => 'metric',
]
]);
$data = json_decode($response->getBody(), true);
$weather = [
'city' => $city,
'temperature' => $data['main']['temp'],
'humidity' => $data['main']['humidity'],
'description' => $data['weather'][0]['description'],
'timestamp' => date('Y-m-d H:i:s'),
];
$promise->resolve($weather);
} catch (\Throwable $e) {
$promise->reject(new \RuntimeException(
"Ошибка получения погоды для {$city}: " . $e->getMessage()
));
}
});
return $promise;
}
/**
* Запускает параллельное получение погоды для всех городов
*/
public function fetchAllWeather(): Promise
{
$promises = [];
foreach ($this->cities as $city) {
$promises[$city] = $this->fetchWeatherForCity($city);
}
return Utils::all($promises);
}
/**
* Сохраняет результаты в файл
*/
private