承接 noith/api-requester 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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/guzzle 7.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:

  • AbstractGetRequest
  • AbstractPostRequest
  • AbstractPutRequest
  • AbstractPatchRequest
  • AbstractDeleteRequest

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:

  • HeadersRequestMutator
  • QueryParametersRequestMutator
  • JsonPayloadFieldsRequestMutator
  • FormParamsRequestMutator
  • JsonBodyRequestMutator
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\RequestException when the response status code is not listed in successStatusCodes()
  • Noith\ApiRequester\ResponseMappingException when response mapping is defined but the selected response transformer does not support mapping
  • GuzzleHttp\Exception\GuzzleException when Guzzle fails to send the request
  • JsonException when JsonResponseTransformer receives a non-empty response body that is not valid JSON
  • LogicException when response mapping is defined but no mapper is available
  • UnexpectedValueException when 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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-16

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固