vanilla/garden-http

一个精简的HTTP客户端库,用于构建RESTful API客户端。


README

CI Tests Packagist Version CLA

Garden HTTP是一个精简的HTTP客户端库,用于构建RESTful API客户端。它旨在允许您在不复制/粘贴大量cURL设置的情况下,也不需要将您的代码库大小翻倍的情况下访问人们的API。您可以直接使用此库作为快速API客户端,也可以扩展HttpClient类以创建您经常使用的结构化API客户端。

安装

Garden HTTP需要PHP 7.4或更高版本以及libcurl

Garden HTTP遵守PSR-4,并可以使用composer安装。只需将vanilla/garden-http添加到您的composer.json中。

Garden请求和响应对象也遵守PSR-7

基本示例

几乎所有使用Garden HTTP的情况都涉及首先创建一个HttpClient对象,然后从中发出请求。您可以看到,以下示例还设置了一个默认头,以便将标准头传递给使用客户端发出的每个请求。

use Garden\Http\HttpClient;

$api = new HttpClient('http://httpbin.org');
$api->setDefaultHeader('Content-Type', 'application/json');

// Get some data from the API.
$response = $api->get('/get'); // requests off of base url
if ($response->isSuccessful()) {
    $data = $response->getBody(); // returns array of json decoded data
}

$response = $api->post('https://httpbin.org/post', ['foo' => 'bar']);
if ($response->isResponseClass('2xx')) {
    // Access the response like an array.
    $posted = $response['json']; // should be ['foo' => 'bar']
}

抛出异常

您可以让HTTP客户端在请求失败时抛出异常。

use Garden\Http\HttpClient;

$api = new HttpClient('https://httpbin.org');
$api->setThrowExceptions(true);

try {
    $api->get('/status/404');
} catch (\Exception $ex) {
    $code = $ex->getCode(); // should be 404
    throw $ex;
}

// If you don't want a specific request to throw.
$response = $api->get("/status/500", [], [], ["throw" => false]);
// But you could throw it yourself.
if (!$response->isSuccessful()) {
    throw $response->asException();
}

将抛出包含失败响应和结构化数据的消息的异常。

try {
    $response = new HttpResponse(501, ["content-type" => "application/json"], '{"message":"Some error occured."}');
    throw $response->asException();
    // Make an exception
} catch (\Garden\Http\HttpResponseException $ex) {
    // Request POST /some/path failed with a response code of 501 and a custom message of "Some error occured."
    $ex->getMessage();
    
    // [
    //      "request" => [
    //          'url' => '/some/path',
    //          'method' => 'POST',
    //      ],
    //      "response" => [
    //          'statusCode' => 501,
    //          'content-type' => 'application/json',
    //          'body' => '{"message":"Some error occured."}',
    //      ]
    // ]
    $ex->getContext();
    
    // It's serializable too.
    json_encode($ex);
}

基本认证

您可以使用auth选项指定基本认证的用户名和密码。

use Garden\Http\HttpClient;

$api = new HttpClient('https://httpbin.org');
$api->setDefaultOption('auth', ['username', 'password123']);

// This request is made with the default authentication set above.
$r1 = $api->get('/basic-auth/username/password123');

// This request overrides the basic authentication.
$r2 = $api->get('/basic-auth/username/password', [], [], ['auth' => ['username', 'password']]);

通过子类化扩展HttpClient

如果您将反复调用相同的API,您可能想通过扩展HttpClient类来创建一个更易于重用的API客户端。

use Garden\Http\HttpClient;
use Garden\Http\HttpHandlerInterface

// A custom HTTP client to access the github API.
class GithubClient extends HttpClient {

    // Set default options in your constructor.
    public function __construct(HttpHandlerInterface $handler = null) {
        parent::__construct('https://api.github.com', $handler);
        $this
            ->setDefaultHeader('Content-Type', 'application/json')
            ->setThrowExceptions(true);
    }

    // Use a default header to authorize every request.
    public function setAccessToken($token) {
        $this->setDefaultHeader('Authorization', "Bearer $token");
    }

    // Get the repos for a given user.
    public function getRepos($username = '') {
        if ($username) {
            return $this->get("/users/$username/repos");
        } else {
            return $this->get("/user/repos"); // my repos
        }
    }

    // Create a new repo.
    public function createRepo($name, $description, $private) {
        return $this->post(
            '/user/repos',
            ['name' => $name, 'description' => $description, 'private' => $private]
        );
    }

    // Get a repo.
    public function getRepo($owner, $repo) {
        return $this->get("/repos/$owner/$repo");
    }

    // Edit a repo.
    public function editRepo($owner, $repo, $name, $description = null, $private = null) {
        return $this->patch(
            "/repos/$owner/$repo",
            ['name' => $name, 'description' => $description, 'private' => $private]
        );
    }

    // Different APIs will return different responses on errors.
    // Override this method to handle errors in a way that is appropriate for the API.
    public function handleErrorResponse(HttpResponse $response, $options = []) {
        if ($this->val('throw', $options, $this->throwExceptions)) {
            $body = $response->getBody();
            if (is_array($body)) {
                $message = $this->val('message', $body, $response->getReasonPhrase());
            } else {
                $message = $response->getReasonPhrase();
            }
            throw new \HttpResponseExceptionException($response, $message);
        }
    }
}

通过中间件扩展HttpClient

HttpClient类有一个addMiddleware()方法,允许您添加一个可以在发送前后修改请求和响应的功能。中间件允许您开发一组可重用的实用工具库,可以与任何客户端一起使用。中间件适用于诸如高级认证、缓存层、CORS支持等。

编写中间件

中间件是一个接受两个参数的可调用对象:一个HttpRequest对象,以及下一个中间件。每个中间件都必须返回一个HttpResponse对象。

function (HttpRequest $request, callable $next): HttpResponse {
    // Do something to the request.
    $request->setHeader('X-Foo', '...');
    
    // Call the next middleware to get the response.
    $response = $next($request);
    
    // Do something to the response.
    $response->setHeader('Cache-Control', 'public, max-age=31536000');
    
    return $response;
}

您必须调用$next,否则请求将不会被HttpClient处理。当然,您可能希望在缓存层等情况下中断请求的处理,在这种情况下,您可以省略对$next的调用。

示例:使用中间件修改请求

考虑以下实现HMAC SHA256哈希的类,该类为需要不仅仅是静态访问令牌的假设API。

class HmacMiddleware {
    protected $apiKey;

    protected $secret;

    public function __construct(string $apiKey, string $secret) {
        $this->apiKey = $apiKey;
        $this->secret = $secret;
    }

    public function __invoke(HttpRequest $request, callable $next): HttpResponse {
        $msg = time().$this->apiKey;
        $sig = hash_hmac('sha256', $msg, $this->secret);

        $request->setHeader('Authorization', "$msg.$sig");

        return $next($request);
    }
}

此中间件为每个请求计算一个新的授权头并将其添加到请求中。然后它调用$next闭包以执行请求的其余部分。

HttpHandlerInterface

在Garden HTTP中,请求是通过HTTP处理器执行的。当前包含的默认处理器使用cURL执行请求。但是,您可以按照自己的方式实现HttpHandlerInterface,并完全改变处理请求的方式。该接口只包含一个方法

public function send(HttpRequest $request): HttpResponse;

该方法旨在将请求转换为响应。要使用它,只需将HttpRequest对象传递给它即可。

您还可以使用自定义处理器与HttpClient一起使用。只需将其传递给构造函数即可。

$api = new HttpClient('https://example.com', new CustomHandler());

检查请求和响应

有时当你收到一个响应时,你想要知道是什么请求生成的。HttpResponse类有一个getRequest()方法用于此。HttpRequest类有一个getResponse()方法用于反向操作。

HttpClient对象抛出的异常是HttpResponseException类的实例。该类有getRequest()getResponse()方法,这样你可以检查异常的请求和响应。这种异常特别有用,因为请求对象是在客户端内部创建的,而不是由程序员直接创建的。

用于测试的模拟

提供了一种HttpHandlerInterface实现和工具,用于模拟请求和响应。

设置

use Garden\Http\HttpClient
use Garden\Http\Mocks\MockHttpHandler;

// Manually apply the handler.
$httpClient = new HttpClient();
$mockHandler = new MockHttpHandler();
$httpClient->setHandler($mockHandler);

// Automatically apply a handler to `HttpClient` instances.
// You can call this again later to retrieve the same handler.
$mockHandler = MockHttpHandler::mock();

// Don't forget this in your phpunit `teardown()`
MockHttpHandler::clearMock();;

// Reset the handler instance
$mockHandler->reset();

模拟请求

use Garden\Http\Mocks\MockHttpHandler;
use Garden\Http\Mocks\MockResponse;

// By default this will return 404 for all requests.
$mockHttp = MockHttpHandler::mock();

$mockHttp
    // Explicit request and response
    ->addMockRequest(
        new \Garden\Http\HttpRequest("GET", "https://domain.com/some/url"),
        new \Garden\Http\HttpResponse(200, ["content-type" => "application/json"], '{"json": "here"}'),
    )
    // Shorthand
    ->addMockRequest(
        "GET https://domain.com/some/url",
        MockResponse::json(["json" => "here"])
    )
    // Even shorter-hand
    // Mocking 200 JSON responses to GET requests is very easy.
    ->addMockRequest(
        "https://domain.com/some/url",
        ["json" => "here"]
    )
    
    // Wildcards
    // Wildcards match with lower priority than explicitly matching requests.
    
    // Explicit wildcard hostname.
    ->addMockRequest("https://*/some/path", MockResponse::success())
    // Implied wildcard hostname.
    ->addMockRequest("/some/path", MockResponse::success())
    // wildcard in path
    ->addMockRequest("https://some-doain.com/some/*", MockResponse::success())
    // Total wildcard
    ->addMockRequest("*", MockResponse::notFound())
;

// Mock multiple requests at once
$mockHttp->mockMulti([
    "GET /some/path" => MockResponse::success()
    "POST /other/path" => MockResponse::json([])
]);

响应序列

在任何可以使用模拟HttpResponse的地方,您也可以使用MockHttpSequence

推入序列的每个项将只返回一次。一旦响应被返回,它将不会再次返回。

如果整个序列耗尽,它将返回404响应。

use Garden\Http\Mocks\MockHttpHandler;
use Garden\Http\Mocks\MockResponse;

$mockHttp = MockHttpHandler::mock();

$mockHttp->mockMulti([
    "GET /some/path" => MockResponse::sequence()
        ->push(new \Garden\Http\HttpResponse(500, [], ""))
        ->push(MockResponse::success())
        ->push(MockResponse::json([])
        ->push([]) // Implied json
    ,
]);

响应函数

您可以通过提供一个可调用对象使模拟动态化。

use Garden\Http\Mocks\MockHttpHandler;
use Garden\Http\Mocks\MockResponse;
use \Garden\Http\HttpRequest;
use \Garden\Http\HttpResponse;

$mockHttp = MockHttpHandler::mock();
$mockHttp->addMockRequest("*", function (\Garden\Http\HttpRequest $request): HttpResponse {
    return MockResponse::json([
        "requestedUrl" => $request->getUrl(),
    ]);
})

对请求的断言

提供了一些工具,可以对已执行的请求进行断言。这对于使用通配符响应特别有用。

use Garden\Http\Mocks\MockHttpHandler;
use Garden\Http\Mocks\MockResponse;
use Garden\Http\HttpRequest;

$mockHttp = MockHttpHandler::mock();

$mockHttp->addMockRequest("*", MockResponse::success());

// Ensure no requests were made.
$mockHttp->assertNothingSent();

// Check that a request was made
$foundRequest = $mockHttp->assertSent(fn (HttpRequest $request) => $request->getUri()->getPath() === "/some/path");

// Check that a request was not made.
$foundRequest = $mockHttp->assertNotSent(fn (HttpRequest $request) => $request->getUri()->getPath() === "/some/path");

// Clear the history (and mocked requests)
$mockHttp->reset();