noith/api-requester
Composer 安装命令:
composer require noith/api-requester
包简介
A typed HTTP client for API with request/response DTO mapping.
README 文档
README
Typed HTTP client for APIs with request classes, response transformation, authentication helpers, and optional response DTO mapping.
Requirements
- PHP 8.1 or newer
- Composer
guzzlehttp/guzzle7.x
Installation
composer require noith/api-requester
Basic Usage
Describe each API endpoint as a request class. The client sends the request, checks the response status code, transforms the response, and returns either the transformed payload or a mapped DTO.
<?php
use Noith\ApiRequester\Client;
use Noith\ApiRequester\Requests\AbstractGetRequest;
final class GetUserRequest extends AbstractGetRequest
{
public function __construct(
private readonly int $id,
) {
}
public function path(): string
{
return "/users/{$this->id}";
}
}
$client = new Client('https://api.example.com');
$payload = $client->send(new GetUserRequest(10));
Requests
Base classes define the HTTP method:
AbstractGetRequestAbstractPostRequestAbstractPutRequestAbstractPatchRequestAbstractDeleteRequest
Every request must implement path(). Optional methods allow you to configure query parameters, expected success statuses, response mapping, and request options. Body formats are enabled with explicit body interfaces.
<?php
use Noith\ApiRequester\Requests\AbstractPostRequest;
use Noith\ApiRequester\Requests\HasJsonBody;
final class CreateUserRequest extends AbstractPostRequest implements HasJsonBody
{
public function __construct(
private readonly string $email,
private readonly string $name,
) {
}
public function path(): string
{
return '/users';
}
public function successStatusCodes(): array
{
return [201];
}
public function body(): array
{
return [
'email' => $this->email,
'name' => $this->name,
];
}
public function shouldSendEmptyBody(): bool
{
return false;
}
}
Query Parameters
Override query() to send query parameters.
public function query(): array
{
return [
'page' => 1,
'limit' => 20,
];
}
AbstractRequest also provides the protected comma() helper for APIs that expect array values as comma-separated strings.
public function query(): array
{
return [
'ids' => $this->comma([1, 2, 3]),
];
}
Per-Request Guzzle Options
Override options() to pass Guzzle options for one request. These options are merged after the request query/body are built and before request mutators and authentication are applied.
use GuzzleHttp\RequestOptions;
public function options(): array
{
return [
RequestOptions::TIMEOUT => 15,
RequestOptions::HEADERS => [
'X-Request' => 'slow-report',
],
];
}
headers and query are merged with the generated request options. Other option keys override existing values.
Lifecycle Hooks
Override prepare() to derive or mutate request state before validation and sending. Use it to set computed fields that other methods depend on.
final class CreateReportRequest extends AbstractPostRequest implements HasJsonBody
{
private string $idempotencyKey;
public function path(): string
{
return '/reports';
}
public function prepare(): void
{
$this->idempotencyKey = bin2hex(random_bytes(16));
}
public function options(): array
{
return [
RequestOptions::HEADERS => [
'Idempotency-Key' => $this->idempotencyKey,
],
];
}
// ...
}
Override validate() to check request state before sending. Throw \InvalidArgumentException for missing or invalid values.
final class SearchRequest extends AbstractGetRequest
{
public function __construct(
private readonly string $query,
) {
}
public function path(): string
{
return '/search';
}
public function validate(): void
{
if (trim($this->query) === '') {
throw new \InvalidArgumentException('Search query must not be empty.');
}
}
}
The call order per request is: prepare() → validate() → send.
Empty JSON Body
Requests that implement HasJsonBody send a JSON body only when body() returns a non-empty array. To force sending an empty JSON object ({}), return true from shouldSendEmptyBody().
public function shouldSendEmptyBody(): bool
{
return true;
}
Other Request Body Formats
Use body interfaces when an endpoint expects a non-JSON body. A request can use only one body format at a time.
Form Params
use Noith\ApiRequester\Requests\AbstractPostRequest;
use Noith\ApiRequester\Requests\HasFormParams;
final class CreateTokenRequest extends AbstractPostRequest implements HasFormParams
{
public function path(): string
{
return '/token';
}
public function formParams(): array
{
return [
'grant_type' => 'client_credentials',
'scope' => 'read',
];
}
}
Multipart
use Noith\ApiRequester\Requests\AbstractPostRequest;
use Noith\ApiRequester\Requests\HasMultipartBody;
final class UploadFileRequest extends AbstractPostRequest implements HasMultipartBody
{
public function path(): string
{
return '/files';
}
public function multipart(): array
{
return [
[
'name' => 'file',
'contents' => fopen('/path/to/file.txt', 'r'),
'filename' => 'file.txt',
],
];
}
}
Raw Body
use Noith\ApiRequester\Requests\AbstractPostRequest;
use Noith\ApiRequester\Requests\HasRawBody;
final class SendXmlRequest extends AbstractPostRequest implements HasRawBody
{
public function path(): string
{
return '/xml';
}
public function rawBody(): mixed
{
return '<request><id>10</id></request>';
}
}
Client Configuration
use Noith\ApiRequester\Client;
$client = new Client(
baseUrl: 'https://api.example.com',
headers: [
'User-Agent' => 'my-app/1.0',
],
guzzleOptions: [
'timeout' => 10,
],
);
The client always sends Accept: application/json by default. Custom headers are merged on top of it.
You can also pass your own GuzzleHttp\ClientInterface implementation. In that case, guzzleOptions cannot be passed at the same time.
use GuzzleHttp\Client as GuzzleClient;
use Noith\ApiRequester\Client;
$http = new GuzzleClient(['timeout' => 5]);
$client = new Client('https://api.example.com', http: $http);
Request Mutators
Request mutators change every outgoing request before it is sent. They can add or override headers, query parameters, JSON body fields, or any other Guzzle option through a custom implementation.
Built-in mutators:
HeadersRequestMutatorQueryParametersRequestMutatorJsonPayloadFieldsRequestMutatorFormParamsRequestMutatorJsonBodyRequestMutator
use Noith\ApiRequester\Client;
use Noith\ApiRequester\RequestMutators\FormParamsRequestMutator;
use Noith\ApiRequester\RequestMutators\HeadersRequestMutator;
use Noith\ApiRequester\RequestMutators\JsonBodyRequestMutator;
use Noith\ApiRequester\RequestMutators\JsonPayloadFieldsRequestMutator;
use Noith\ApiRequester\RequestMutators\QueryParametersRequestMutator;
$client = new Client(
baseUrl: 'https://api.example.com',
requestMutators: [
new HeadersRequestMutator([
'X-Tenant' => 'tenant-1',
]),
new QueryParametersRequestMutator([
'locale' => 'en',
]),
new JsonPayloadFieldsRequestMutator([
'trace_id' => 'trace-1',
]),
new FormParamsRequestMutator([
'cid' => '123',
]),
new JsonBodyRequestMutator([
'client_id' => '123',
]),
],
);
FormParamsRequestMutator merges fields into RequestOptions::FORM_PARAMS regardless of the request type. It creates the key when it is not yet present.
new FormParamsRequestMutator(['cid' => '123'])
JsonBodyRequestMutator merges fields into RequestOptions::JSON regardless of the request type. It creates the key when it is not yet present. Unlike JsonPayloadFieldsRequestMutator, it applies to every request, not only those that implement HasJsonBody.
new JsonBodyRequestMutator(['client_id' => '123'])
Mutators are applied after the request builds its own query/body and before the authenticator is applied. If several mutators write the same value, later mutators override earlier ones.
JsonPayloadFieldsRequestMutator only changes requests that explicitly implement HasJsonBody.
Per-Request Mutators
Pass mutators directly to send() to apply them only for that call. They run after all client-level mutators, so they take priority over them.
$client->send(
new GetUserRequest(10),
new HeadersRequestMutator(['X-Tenant' => 'tenant-1']),
);
For custom behavior, implement Noith\ApiRequester\RequestMutators\RequestMutatorInterface.
use GuzzleHttp\RequestOptions;
use Noith\ApiRequester\RequestMutators\RequestMutatorInterface;
use Noith\ApiRequester\Requests\AbstractRequest;
final class CorrelationIdRequestMutator implements RequestMutatorInterface
{
public function mutate(array $options, AbstractRequest $request): array
{
$options[RequestOptions::HEADERS]['X-Correlation-Id'] = bin2hex(random_bytes(16));
return $options;
}
}
Authentication
The package includes several authenticators.
Bearer Token
use Noith\ApiRequester\Auth\BearerTokenAuthenticator;
use Noith\ApiRequester\Client;
$client = new Client(
baseUrl: 'https://api.example.com',
authenticator: new BearerTokenAuthenticator('token'),
);
Basic Auth
use Noith\ApiRequester\Auth\BasicAuthenticator;
use Noith\ApiRequester\Client;
$client = new Client(
baseUrl: 'https://api.example.com',
authenticator: new BasicAuthenticator('username', 'password'),
);
API Key Header
use Noith\ApiRequester\Auth\HeaderApiKeyAuthenticator;
use Noith\ApiRequester\Client;
$client = new Client(
baseUrl: 'https://api.example.com',
authenticator: new HeaderApiKeyAuthenticator('X-Api-Key', 'api-key'),
);
API Key Query Parameter
use Noith\ApiRequester\Auth\QueryApiKeyAuthenticator;
use Noith\ApiRequester\Client;
$client = new Client(
baseUrl: 'https://api.example.com',
authenticator: new QueryApiKeyAuthenticator('api_key', 'api-key'),
);
For custom authentication, implement Noith\ApiRequester\Auth\AuthenticatorInterface.
Response Transformers
By default, the client uses JsonResponseTransformer: empty response bodies become null, and non-empty bodies are decoded with json_decode(..., JSON_THROW_ON_ERROR).
use Noith\ApiRequester\Client;
use Noith\ApiRequester\ResponseTransformers\JsonResponseTransformer;
$client = new Client(
baseUrl: 'https://api.example.com',
responseTransformer: new JsonResponseTransformer(),
);
For non-JSON responses, use another transformer.
Each transformer declares whether its result can be passed to MapperInterface through supportsMapping(). If a request defines response mapping and the selected transformer does not support mapping, the client throws ResponseMappingException before calling the mapper.
Raw Body
RawBodyResponseTransformer returns the response body as a string and does not support response mapping.
use Noith\ApiRequester\Client;
use Noith\ApiRequester\ResponseTransformers\RawBodyResponseTransformer;
$client = new Client(
baseUrl: 'https://api.example.com',
responseTransformer: new RawBodyResponseTransformer(),
);
PSR Response
PsrResponseTransformer returns the original Psr\Http\Message\ResponseInterface and does not support response mapping. This is useful for downloads, streams, headers, or responses that should be handled outside the package.
use Noith\ApiRequester\Client;
use Noith\ApiRequester\ResponseTransformers\PsrResponseTransformer;
$client = new Client(
baseUrl: 'https://api.example.com',
responseTransformer: new PsrResponseTransformer(),
);
Per-Request Transformer
A request can override the client transformer by implementing responseTransformer().
use Noith\ApiRequester\Requests\AbstractGetRequest;
use Noith\ApiRequester\ResponseTransformers\RawBodyResponseTransformer;
use Noith\ApiRequester\ResponseTransformers\ResponseTransformerInterface;
final class DownloadReportRequest extends AbstractGetRequest
{
public function path(): string
{
return '/reports/latest.csv';
}
public function responseTransformer(): ?ResponseTransformerInterface
{
return new RawBodyResponseTransformer();
}
}
For custom transformation, implement Noith\ApiRequester\ResponseTransformers\ResponseTransformerInterface.
use Noith\ApiRequester\ResponseTransformers\ResponseTransformerInterface;
use Psr\Http\Message\ResponseInterface;
final class XmlResponseTransformer implements ResponseTransformerInterface
{
public function transform(ResponseInterface $response): mixed
{
return simplexml_load_string((string) $response->getBody());
}
public function supportsMapping(): bool
{
return true;
}
}
Response Mapping
By default, send() returns decoded JSON as an array, scalar value, or null for an empty response body. If a custom response transformer is configured, send() returns that transformer's result.
To map responses into DTOs, implement MapperInterface, make the request implement HasResponse, and return the target class from responseDataClass().
<?php
use Noith\ApiRequester\MapperInterface;
use Noith\ApiRequester\Requests\AbstractGetRequest;
use Noith\ApiRequester\Requests\HasResponse;
final readonly class UserData
{
public function __construct(
public int $id,
public string $email,
) {
}
}
final class SimpleMapper implements MapperInterface
{
public function map(mixed $payload, string $dataClass): mixed
{
return new $dataClass(...$payload);
}
public function mapCollection(array $payload, string $dataClass): array
{
return array_map(
fn (array $item): mixed => $this->map($item, $dataClass),
$payload,
);
}
public function mapListing(mixed $payload, string $listingClass, array $items): mixed
{
return new $listingClass(
items: $items,
total: $payload['total'],
page: $payload['page'],
);
}
}
final class GetUserRequest extends AbstractGetRequest implements HasResponse
{
public function __construct(
private readonly int $id,
) {
}
public function path(): string
{
return "/users/{$this->id}";
}
public function responseDataClass(): string
{
return UserData::class;
}
}
Pass the mapper to the client:
$client = new Client(
baseUrl: 'https://api.example.com',
mapper: new SimpleMapper(),
);
$user = $client->send(new GetUserRequest(10));
A request can override the client mapper by implementing mapper().
Response Collections
For responses that are plain arrays of DTO items, implement HasResponseCollection.
use Noith\ApiRequester\Requests\AbstractGetRequest;
use Noith\ApiRequester\Requests\HasResponseCollection;
final class GetUsersRequest extends AbstractGetRequest implements HasResponseCollection
{
public function path(): string
{
return '/users';
}
public function responseDataClass(): string
{
return UserData::class;
}
}
The client passes the decoded response payload to MapperInterface::mapCollection().
Response Listings
For wrapped listing responses, implement HasResponseListing. It extends HasResponseCollection and adds the listing wrapper DTO class and the payload key that contains items.
use Noith\ApiRequester\Requests\AbstractGetRequest;
use Noith\ApiRequester\Requests\HasResponseListing;
final readonly class UserListingData
{
/**
* @param UserData[] $items
*/
public function __construct(
public array $items,
public int $total,
public int $page,
) {
}
}
final class GetUsersRequest extends AbstractGetRequest implements HasResponseListing
{
public function path(): string
{
return '/users';
}
public function responseDataClass(): string
{
return UserData::class;
}
public function responseListingClass(): string
{
return UserListingData::class;
}
public function responseItemsKey(): string
{
return 'items';
}
}
For a payload like:
{
"items": [
{"id": 1, "email": "first@example.com"},
{"id": 2, "email": "second@example.com"}
],
"total": 2,
"page": 1
}
The client maps items through MapperInterface::mapCollection() and then passes the original payload, listing class, and mapped items to MapperInterface::mapListing().
Error Handling
Client::send() may throw:
Noith\ApiRequester\RequestExceptionwhen the response status code is not listed insuccessStatusCodes()Noith\ApiRequester\ResponseMappingExceptionwhen response mapping is defined but the selected response transformer does not support mappingGuzzleHttp\Exception\GuzzleExceptionwhen Guzzle fails to send the requestJsonExceptionwhenJsonResponseTransformerreceives a non-empty response body that is not valid JSONLogicExceptionwhen response mapping is defined but no mapper is availableUnexpectedValueExceptionwhen a collection or listing response has an invalid payload shape
RequestException exposes the original request, actual status code, response body, and expected status codes.
use Noith\ApiRequester\RequestException;
try {
$result = $client->send($request);
} catch (RequestException $exception) {
$statusCode = $exception->statusCode;
$body = $exception->responseBody;
}
Development
Install dependencies:
composer install
Validate Composer metadata:
composer validate
Run tests if a PHPUnit configuration or test suite is added:
vendor/bin/phpunit
License
MIT
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-16