强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 状态码参考

状态码含义场景
200OK成功
201Created资源已创建
204No Content删除成功
301Moved Permanently永久重定向
302Found临时重定向
400Bad Request请求格式错误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
405Method Not Allowed方法不允许
422Unprocessable Entity验证失败
429Too Many Requests限流
500Internal Server Error服务器错误
502Bad Gateway网关错误
503Service 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 章 — 测试