发起 HTTP 请求
在构建现代应用程序时,你经常需要向外部 API 发起 HTTP 请求。本文演示如何在 Yii3 应用程序中使用 Guzzle 和 PSR 接口发起 HTTP 请求。
什么是 HTTP 的 PSR 接口
PHP-FIG(PHP 框架互操作性组)为 HTTP 处理定义了几个 PSR 标准:
- PSR-7:用于请求和响应的 HTTP 消息接口
- PSR-17:用于创建 PSR-7 消息对象的 HTTP 工厂接口
- PSR-18:用于发送 PSR-7 请求并返回 PSR-7 响应的 HTTP 客户端接口
使用这些接口可以确保你的代码与框架无关,并遵循已建立的 PHP 标准。
安装
安装支持 PSR-18 和 PSR-17 工厂的 Guzzle HTTP 客户端:
composer require guzzlehttp/guzzle
composer require guzzlehttp/psr7基本用法
简单的 GET 请求
以下是如何使用 PSR-18 接口发起基本 GET 请求:
<?php
declare(strict_types=1);
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class ApiService
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
) {
}
public function fetchUserData(int $userId): ResponseInterface
{
$request = $this->requestFactory->createRequest(
'GET',
"https://example.com/users/{$userId}"
);
return $this->httpClient->sendRequest($request);
}
}带 JSON 数据的 POST 请求
以下是发起带 JSON 负载的 POST 请求的示例:
<?php
declare(strict_types=1);
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class UserService
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
) {
}
public function createUser(array $userData): ResponseInterface
{
$jsonData = json_encode($userData, JSON_THROW_ON_ERROR);
$stream = $this->streamFactory->createStream($jsonData);
$request = $this->requestFactory->createRequest('POST', 'https://example.com/users')
->withHeader('Content-Type', 'application/json')
->withHeader('Accept', 'application/json')
->withBody($stream);
return $this->httpClient->sendRequest($request);
}
}在 Yii3 中的配置
容器配置
在 DI 容器中配置 HTTP 客户端和 PSR 工厂:
<?php
declare(strict_types=1);
// config/common/http-client.php
use GuzzleHttp\Client;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
return [
ClientInterface::class => [
'class' => Client::class,
'__construct()' => [
'config' => [
'timeout' => 30,
'connect_timeout' => 10,
],
],
],
// Configure PSR-17 factories - these will depend on your chosen PSR-7 implementation
RequestFactoryInterface::class => static function (): RequestFactoryInterface {
return new \GuzzleHttp\Psr7\HttpFactory();
},
ResponseFactoryInterface::class => static function (): ResponseFactoryInterface {
return new \GuzzleHttp\Psr7\HttpFactory();
},
StreamFactoryInterface::class => static function (): StreamFactoryInterface {
return new \GuzzleHttp\Psr7\HttpFactory();
},
UriFactoryInterface::class => static function (): UriFactoryInterface {
return new \GuzzleHttp\Psr7\HttpFactory();
},
];带错误处理的服务
以下是一个更健壮的服务示例,具有适当的错误处理:
<?php
declare(strict_types=1);
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Log\LoggerInterface;
class WeatherService
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
private LoggerInterface $logger,
private string $apiKey,
) {
}
public function getCurrentWeather(string $city): ?array
{
try {
$request = $this->requestFactory->createRequest(
'GET',
"https://api.openweathermap.org/data/2.5/weather?q={$city}&appid={$this->apiKey}&units=metric"
);
$response = $this->httpClient->sendRequest($request);
if ($response->getStatusCode() !== 200) {
$this->logger->warning('Weather API returned non-200 status', [
'status_code' => $response->getStatusCode(),
'city' => $city,
]);
return null;
}
$data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
return $data;
} catch (ClientExceptionInterface $e) {
$this->logger->error('HTTP client error when fetching weather data', [
'city' => $city,
'error' => $e->getMessage(),
]);
return null;
} catch (\JsonException $e) {
$this->logger->error('Failed to decode weather API response', [
'city' => $city,
'error' => $e->getMessage(),
]);
return null;
}
}
}高级用法
使用中间件
Guzzle 支持用于横切关注点的中间件,如身份验证、日志记录或重试:
<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;
class HttpClientFactory
{
public function __construct(
private LoggerInterface $logger,
) {
}
public function createClient(): Client
{
$stack = HandlerStack::create();
// Add request/response logging middleware
$stack->push(Middleware::log(
$this->logger,
new \GuzzleHttp\MessageFormatter('HTTP {method} {uri} - {code} {phrase}')
));
// Add retry middleware
$stack->push(Middleware::retry(
function (int $retries, RequestInterface $request) {
return $retries < 3;
}
));
return new Client([
'handler' => $stack,
'timeout' => 30,
]);
}
}异步请求
为了在发起多个请求时获得更好的性能,你可以使用异步请求:
Note:异步功能不是 PSR 接口的一部分,因此此代码明确依赖于 Guzzle。
<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
class BatchApiService
{
public function __construct(
private Client $httpClient,
) {
}
/**
* @param array<int> $userIds
* @return array<ResponseInterface>
*/
public function fetchMultipleUsers(array $userIds): array
{
$promises = [];
foreach ($userIds as $userId) {
$promises[$userId] = $this->httpClient->getAsync(
"https://example.com/users/{$userId}"
);
}
// Wait for all requests to complete
$responses = \GuzzleHttp\Promise\settle($promises)->wait();
$results = [];
foreach ($responses as $userId => $response) {
if ($response['state'] === PromiseInterface::FULFILLED) {
$results[$userId] = $response['value'];
}
}
return $results;
}
}测试 HTTP 客户端
在测试发起 HTTP 请求的服务时,你可以使用 Guzzle 的 MockHandler:
<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
class WeatherServiceTest extends TestCase
{
public function testGetCurrentWeatherSuccess(): void
{
$mockHandler = new MockHandler([
new Response(200, [], json_encode([
'name' => 'London',
'main' => ['temp' => 20.5],
])),
]);
$handlerStack = HandlerStack::create($mockHandler);
$client = new Client(['handler' => $handlerStack]);
$service = new WeatherService(
$client,
new \GuzzleHttp\Psr7\HttpFactory(),
new \GuzzleHttp\Psr7\HttpFactory(),
$this->createMock(\Psr\Log\LoggerInterface::class),
'test-api-key'
);
$result = $service->getCurrentWeather('London');
$this->assertNotNull($result);
$this->assertSame('London', $result['name']);
$this->assertSame(20.5, $result['main']['temp']);
}
}最佳实践
使用 PSR 接口:始终针对 PSR 接口进行类型提示,而不是具体实现,以获得更好的可测试性和灵活性。
优雅地处理错误:始终将 HTTP 请求包装在 try-catch 块中,并适当处理网络故障。
配置超时:设置合理的连接和请求超时,以防止请求挂起。
记录请求:使用中间件或手动日志记录来跟踪 API 调用,以便调试和监控。
使用依赖注入:通过 DI 容器注入 HTTP 客户端和工厂,而不是直接创建它们。
在测试中使用模拟:使用 Guzzle 的 MockHandler 或类似工具来测试 HTTP 客户端代码,而无需发起真实的网络请求。
通过遵循这些模式并使用 PSR 接口,你将在 Yii3 应用程序中创建可维护、可测试和可互操作的 HTTP 客户端代码。