第 19 章 — HTTP 编程
第 19 章 — HTTP 编程:cURL、Guzzle、PSR-7/15/17/18
19.1 原生 cURL
<?php
// 基本 GET 请求
$ch = curl_init('https://api.example.com/users');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Authorization: Bearer ' . $token,
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// POST JSON
$ch = curl_init('https://api.example.com/users');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['name' => 'Alice', 'email' => '[email protected]']),
]);
$response = curl_exec($ch);
19.2 Guzzle HTTP 客户端
Guzzle 是 PHP 最流行的 HTTP 客户端库,符合 PSR-18 标准。
composer require guzzlehttp/guzzle
19.2.1 基本用法
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 5.0,
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token,
],
]);
// GET
$response = $client->get('/users', [
'query' => ['page' => 1, 'per_page' => 20],
]);
$users = json_decode($response->getBody(), true);
// POST
$response = $client->post('/users', [
'json' => ['name' => 'Alice', 'email' => '[email protected]'],
]);
$user = json_decode($response->getBody(), true);
// PUT
$client->put('/users/1', [
'json' => ['name' => 'Alice Updated'],
]);
// DELETE
$client->delete('/users/1');
// 文件上传
$response = $client->post('/upload', [
'multipart' => [
[
'name' => 'file',
'contents' => fopen('/path/to/file.pdf', 'r'),
'filename' => 'document.pdf',
],
],
]);
19.2.2 错误处理
<?php
try {
$response = $client->get('/users/999');
} catch (ClientException $e) {
// 4xx 错误
$statusCode = $e->getResponse()->getStatusCode();
$body = json_decode($e->getResponse()->getBody(), true);
echo "Client error {$statusCode}: {$body['message']}";
} catch (ServerException $e) {
// 5xx 错误
echo "Server error: " . $e->getMessage();
} catch (\GuzzleHttp\Exception\ConnectException $e) {
// 连接失败
echo "Connection failed: " . $e->getMessage();
}
19.2.3 中间件
<?php
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
$stack = HandlerStack::create();
// 日志中间件
$stack->push(Middleware::log(
$logger,
new \GuzzleHttp\MessageFormatter('{method} {uri} -> {code}')
));
// 重试中间件
$stack->push(Middleware::retry(
function ($retries, $request, $response, $exception) {
return $retries < 3 && ($response && $response->getStatusCode() >= 500);
},
function ($retries) {
return 1000 * $retries; // 指数退避
}
));
// 请求头中间件
$stack->push(Middleware::mapRequest(function ($request) {
return $request->withHeader('X-Request-ID', bin2hex(random_bytes(16)));
}));
$client = new Client(['handler' => $stack]);
19.3 PSR 标准
19.3.1 PSR-7:HTTP 消息接口
<?php
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Response;
use Psr\Http\Message\UriInterface;
// 不可变请求对象
$request = new Request('GET', 'https://api.example.com/users');
$request = $request
->withHeader('Accept', 'application/json')
->withHeader('Authorization', 'Bearer ' . $token);
echo $request->getMethod(); // GET
echo $request->getUri(); // https://api.example.com/users
echo $request->getHeaderLine('Accept'); // application/json
// 响应
$response = new Response(200, ['Content-Type' => 'application/json']);
$response->getBody()->write(json_encode(['users' => []]));
echo $response->getStatusCode(); // 200
$body = (string) $response->getBody();
19.3.2 PSR-15:HTTP 服务器请求处理器
<?php
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface;
// 中间件接口
class AuthMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
$token = $request->getHeaderLine('Authorization');
if (!$this->validateToken($token)) {
return new Response(401, [], json_encode(['error' => 'Unauthorized']));
}
// 注入用户信息
$request = $request->withAttribute('user', $this->getUser($token));
return $handler->handle($request);
}
}
class LoggingMiddleware implements MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
$start = microtime(true);
$response = $handler->handle($request);
$elapsed = round((microtime(true) - $start) * 1000, 2);
$this->logger->info(sprintf(
'%s %s -> %d (%.2fms)',
$request->getMethod(),
$request->getUri()->getPath(),
$response->getStatusCode(),
$elapsed,
));
return $response;
}
}
19.3.3 PSR-17:HTTP 工厂
<?php
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
class ApiClient
{
public function __construct(
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly ClientInterface $httpClient,
) {}
public function createUser(array $data): array
{
$request = $this->requestFactory
->createRequest('POST', 'https://api.example.com/users')
->withHeader('Content-Type', 'application/json')
->withBody($this->streamFactory->createStream(json_encode($data)));
$response = $this->httpClient->sendRequest($request);
return json_decode((string) $response->getBody(), true);
}
}
19.3.4 PSR-18:HTTP 客户端接口
<?php
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class MyHttpClient implements ClientInterface
{
public function sendRequest(RequestInterface $request): ResponseInterface
{
// 实现自定义 HTTP 客户端逻辑
// 或包装现有的 cURL 调用
}
}
19.4 HTTP 状态码参考
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | OK | 成功 |
| 201 | Created | 资源已创建 |
| 204 | No Content | 删除成功 |
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 400 | Bad Request | 请求格式错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 方法不允许 |
| 422 | Unprocessable Entity | 验证失败 |
| 429 | Too Many Requests | 限流 |
| 500 | Internal Server Error | 服务器错误 |
| 502 | Bad Gateway | 网关错误 |
| 503 | Service Unavailable | 服务不可用 |
19.5 业务场景:API 客户端封装
<?php
declare(strict_types=1);
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
class ApiClient
{
private Client $client;
public function __construct(
string $baseUrl,
string $apiKey,
float $timeout = 10.0,
) {
$this->client = new Client([
'base_uri' => $baseUrl,
'timeout' => $timeout,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => "Bearer {$apiKey}",
],
]);
}
public function get(string $uri, array $query = []): array
{
return $this->request('GET', $uri, ['query' => $query]);
}
public function post(string $uri, array $data = []): array
{
return $this->request('POST', $uri, ['json' => $data]);
}
public function put(string $uri, array $data = []): array
{
return $this->request('PUT', $uri, ['json' => $data]);
}
public function delete(string $uri): array
{
return $this->request('DELETE', $uri);
}
private function request(string $method, string $uri, array $options = []): array
{
try {
$response = $this->client->request($method, $uri, $options);
$body = (string) $response->getBody();
return $body ? json_decode($body, true) : [];
} catch (ClientException $e) {
$statusCode = $e->getResponse()->getStatusCode();
$body = json_decode((string) $e->getResponse()->getBody(), true);
throw new ApiException($body['message'] ?? 'API Error', $statusCode);
}
}
}
19.6 扩展阅读
上一章:第 18 章 — 文件系统 下一章:第 20 章 — 测试