PHPackages                             wimski/http-requests - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. wimski/http-requests

ActiveLibrary[HTTP &amp; Networking](/categories/http)

wimski/http-requests
====================

Opinionated starter kit for making HTTP requests (API implementations)

1.1.0(2mo ago)0215↑80%MITPHPPHP ^8.3CI passing

Since Apr 2Pushed 2mo agoCompare

[ Source](https://github.com/wimski/http-requests)[ Packagist](https://packagist.org/packages/wimski/http-requests)[ RSS](/packages/wimski-http-requests/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (2)Dependencies (20)Versions (3)Used By (0)

[![PHPStan](https://github.com/wimski/http-requests/actions/workflows/phpstan.yml/badge.svg)](https://github.com/wimski/http-requests/actions/workflows/phpstan.yml)[![PHPUnit](https://github.com/wimski/http-requests/actions/workflows/phpunit.yml/badge.svg)](https://github.com/wimski/http-requests/actions/workflows/phpunit.yml)[![Coverage](https://raw.githubusercontent.com/wimski/artifacts/master/wimski/http-requests/master/coverage.svg)](https://github.com/wimski/http-requests/actions/workflows/coverage.yml)

HTTP Requests
=============

[](#http-requests)

An opinionated, extensible PHP library for building type-safe HTTP API clients using PSR standards.

Overview
--------

[](#overview)

This library provides a structured framework for implementing HTTP API clients with a focus on:

- **Type Safety**: Full generic type support with PHPDoc annotations
- **Extensibility**: Interface-driven architecture allowing custom implementations
- **PSR Compliance**: Built on PSR-7, PSR-17, and PSR-18 standards
- **Separation of Concerns**: Clear separation between requests, responses, and data transformation

Requirements
------------

[](#requirements)

- PHP 8.3+
- PSR-7 HTTP Message implementation
- PSR-17 HTTP Factory implementation
- PSR-18 HTTP Client implementation

[Discovery](https://github.com/php-http/discovery) is recommended for agnostic PSR HTTP class usage.

Installation
------------

[](#installation)

```
composer require wimski/http-requests
```

Core Concepts
-------------

[](#core-concepts)

### Architecture

[](#architecture)

The library follows a layered architecture:

1. **Client Layer**: `Client` orchestrates the request/response lifecycle
2. **Request Layer**: `RequestInterface` implementations define API endpoints
3. **Factory Layer**: Factories transform between PSR and domain objects
4. **Response Layer**: `ResponseInterface` implementations wrap API responses
5. **Data Layer**: DTOs/entities representing API resources

### Key Components

[](#key-components)

- **`ClientInterface`**: Main entry point for making HTTP requests
- **`RequestInterface`**: Defines HTTP request specifications
- **`ResponseInterface`**: Wraps response data with type safety
- **`DataFactoryInterface`**: Transforms arrays into typed objects
- **`ResponseBodyFactoryInterface`**: Parses HTTP response bodies
- **`StreamFactoryInterface`**: Serializes request bodies

Basic Usage
-----------

[](#basic-usage)

### 1. Define Your Data Object

[](#1-define-your-data-object)

```
readonly class User
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
    ) {}
}
```

### 2. Create a Request

[](#2-create-a-request)

```
use Wimski\HttpRequests\Requests\AbstractRequest;
use Wimski\HttpRequests\Responses\SingleResponse;

/**
 * @extends AbstractRequest
 */
class GetUserRequest extends AbstractRequest
{
    public function __construct(
        protected readonly int $userId,
    ) {}

    public function getUri(): string
    {
        return "/users/{$this->userId}";
    }

    public function getHeaders(): array
    {
        return [
            'accept' => 'application/json',
        ];
    }

    public function getResponseDataClass(): string
    {
        return User::class;
    }

    public function getResponseClass(): string
    {
        return SingleResponse::class;
    }
}
```

### 3. Set Up the Client

[](#3-set-up-the-client)

```
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Wimski\HttpRequests\Client;
use Wimski\HttpRequests\Factories\DataFactory;
use Wimski\HttpRequests\Factories\FormStreamFactory;
use Wimski\HttpRequests\Factories\JsonResponseBodyFactory;
use Wimski\HttpRequests\Factories\JsonStreamFactory;
use Wimski\HttpRequests\Factories\RequestFactory;
use Wimski\HttpRequests\Factories\ResponseBodyFactory;use Wimski\HttpRequests\Factories\ResponseFactory;
use Wimski\HttpRequests\Factories\SingleResponseFactory;
use Wimski\HttpRequests\Factories\UriFactory;

$streamFactory = Psr17FactoryDiscovery::findStreamFactory();

$client = new Client(
    httpClient: HttpClientDiscovery::find(),
    requestFactory: new RequestFactory(
        uriFactory: new UriFactory(
            httpUriFactory: Psr17FactoryDiscovery::findUriFactory(),
            baseUri: 'https://api.example.com',
        ),
        httpRequestFactory: Psr17FactoryDiscovery::findRequestFactory(),
        new StreamFactory(
            new FormStreamFactory($streamFactory),
            new JsonStreamFactory($streamFactory),
            // Add your custom stream factories here
        ),
    ),
    responseFactory: new ResponseFactory(
        new SingleResponseFactory(
            responseBodyFactory: new ResponseBodyFactory(
                new JsonResponseBodyFactory(),
                // Add your custom response body factories here
            ),
            dataFactory: new DataFactory(
                // Add your custom data factories here
            ),
        ),
        // Add your custom response factories here
    ),
);
```

### 4. Make Requests

[](#4-make-requests)

```
$request = new GetUserRequest(userId: 123);

/** @var SingleResponse $response */
$response = $client->request($request);

$user = $response->getData();

echo $user->name; // Type-safe access
```

Extending the Library
---------------------

[](#extending-the-library)

### Custom Data Factory

[](#custom-data-factory)

Implement `DataFactoryInterface` to control how arrays are transformed into objects:

```
use Wimski\HttpRequests\Contracts\Factories\DataFactoryInterface;

readonly class SymfonySerializerDataFactory implements DataFactoryInterface
{
    public function __construct(
        protected SerializerInterface $serializer,
    ) {}

    public function supports(string $class): bool
    {
        // Support all classes in your namespace
        return str_starts_with($class, 'App\\Api\\Data\\');
    }

    public function make(string $class, array $data): object
    {
        return $this->serializer->denormalize($data, $class);
    }
}
```

Register it with the `DataFactory`:

```
$dataFactory = new DataFactory(
    new SymfonySerializerDataFactory($serializer),
    // Fallback factories...
);
```

### Custom Response Body Factory

[](#custom-response-body-factory)

Support different content types by implementing `ResponseBodyFactoryInterface`:

```
use Psr\Http\Message\ResponseInterface as HttpResponseInterface;
use Wimski\HttpRequests\Contracts\Factories\ResponseBodyFactoryInterface;
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;

readonly class XmlResponseBodyFactory implements ResponseBodyFactoryInterface
{
    public function supports(string $accept): bool
    {
        return $accept === 'application/xml';
    }

    public function make(HttpResponseInterface $httpResponse, RequestInterface $request): array
    {
        // Parse the $httpResponse->getBody()->getContents() string as XML
        // and transform it into an array
    }
}
```

### Custom Stream Factory

[](#custom-stream-factory)

Handle different request body formats by implementing `StreamFactoryInterface`, and probably extending `AbstractStreamFactory`:

```
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;
use Wimski\HttpRequests\Factories\AbstractStreamFactory;

readonly class XmlStreamFactory extends AbstractStreamFactory
{
    public function supports(string $contentType): bool
    {
        return $contentType === 'application/xml';
    }

    public function make(RequestInterface $request): string
    {
        // Transform $request->getBody() array into an XML string
    }
}
```

### Multi-Item Responses

[](#multi-item-responses)

For endpoints returning collections, use `MultiResponse`:

```
use App\Api\Data\User;
use Wimski\HttpRequests\Requests\AbstractRequest;
use Wimski\HttpRequests\Responses\MultiResponse;

/**
 * @extends AbstractRequest
 */
class ListUsersRequest extends AbstractRequest
{
    public function getUri(): string
    {
        return '/users';
    }

    public function getHeaders(): array
    {
        return ['accept' => 'application/json'];
    }

    public function getResponseDataClass(): string
    {
        return User::class;
    }

    public function getResponseClass(): string
    {
        return MultiResponse::class;
    }
}
```

Create a custom `MultiResponseFactory`:

```
use Wimski\HttpRequests\Factories\MultiResponseFactory;

readonly class ApiMultiResponseFactory extends MultiResponseFactory
{
    protected function makeData(array $body, string $class): array
    {
        // Assuming API returns: {"data": [...]}
        $items = $body['data'] ?? [];

        return array_map(
            fn(array $item) => $this->dataFactory->make($class, $item),
            $items,
        );
    }

    protected function makePagination(array $body): ?ResponsePaginationInterface
    {
        // Extract pagination metadata if present
        if (!isset($body['meta'])) {
            return null;
        }

        // ApiResponsePagination implements ResponsePaginationInterface
        return new ApiResponsePagination(
            currentPage: $body['meta']['current_page'],
            totalPages: $body['meta']['total_pages'],
            perPage: $body['meta']['per_page'],
            total: $body['meta']['total'],
        );
    }
}
```

### Pagination Support

[](#pagination-support)

Implement pagination interfaces for paginated requests:

```
use Wimski\HttpRequests\Contracts\Requests\RequestPaginationInterface;

readonly class ApiRequestPagination implements RequestPaginationInterface
{
    public function __construct(
        protected int $page = 1,
        protected int $perPage = 20,
    ) {}

    public function getPagination(): array
    {
        return [
            'page'     => $this->page,
            'per_page' => $this->perPage,
        ];
    }
}
```

Use it in your request:

```
$request = new ListUsersRequest();
$request->setPagination(new ApiRequestPagination(page: 2, perPage: 50));

$response = $client->request($request);
```

### Custom Request Methods

[](#custom-request-methods)

Override methods in `AbstractRequest` for specific behaviors:

```
use Wimski\HttpRequests\Enums\HttpRequestMethodEnum;
use Wimski\HttpRequests\Requests\AbstractRequest;

class CreateUserRequest extends AbstractRequest
{
    public function __construct(
        protected readonly string $name,
        protected readonly string $email,
    ) {}

    public function getMethod(): HttpRequestMethodEnum
    {
        return HttpRequestMethodEnum::POST;
    }

    public function getUri(): string
    {
        return '/users';
    }

    public function getHeaders(): array
    {
        return [
            'accept'       => 'application/json',
            'content-type' => 'application/json',
        ];
    }

    public function getBody(): array
    {
        return [
            'name'  => $this->name,
            'email' => $this->email,
        ];
    }

    public function getQuery(): array
    {
        return ['type' => 'json'];
    }

    public function getResponseDataClass(): string
    {
        return User::class;
    }

    public function getResponseClass(): string
    {
        return SingleResponse::class;
    }
}
```

### Query Parameters

[](#query-parameters)

Add query parameters via `getQuery()`:

```
// /users/search?q={$query}&role={$role}
class SearchUsersRequest extends AbstractRequest
{
    public function __construct(
        protected readonly string $query,
        protected readonly ?string $role = null,
    ) {}

    public function getUri(): string
    {
        return '/users/search';
    }

    public function getQuery(): array
    {
        $query = ['q' => $this->query];

        if ($this->role !== null) {
            $query['role'] = $this->role;
        }

        return $query;
    }

    // ... other methods
}
```

### Custom Client Behavior

[](#custom-client-behavior)

Extend `Client` to add custom behavior:

```
use Psr\Http\Message\RequestInterface as HttpRequestInterface;
use Psr\Http\Message\ResponseInterface as HttpResponseInterface;
use Wimski\HttpRequests\Client;
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;

readonly class AuthenticatedClient extends Client
{
    public function __construct(
        protected string $apiToken,
        // ... parent dependencies
    ) {
        parent::__construct(...);
    }

    protected function makeRequest(RequestInterface $request): HttpRequestInterface
    {
        return parent::makeRequest($request)
            ->withHeader('Authorization', "Bearer {$this->apiToken}");
    }

    protected function validateStatusCode(HttpResponseInterface $httpResponse, HttpRequestInterface $httpRequest): void
    {
        // Custom status code handling
        $statusCode = $httpResponse->getStatusCode();

        if ($statusCode === 401) {
            throw new AuthenticationException('Invalid API token');
        }

        parent::validateStatusCode($httpResponse, $httpRequest);
    }
}
```

### Custom Response Types

[](#custom-response-types)

Create custom response classes for specialized use cases:

```
use Wimski\HttpRequests\Contracts\Responses\ResponseInterface;

/**
 * @template TData of object
 * @implements ResponseInterface
 */
readonly class PagedResponse implements ResponseInterface
{
    /**
     * @param list $data
     */
    public function __construct(
        protected array $data,
        protected int $currentPage,
        protected int $totalPages,
        protected int $total,
    ) {}

    public function getData(): array
    {
        return $this->data;
    }

    public function getPagination(): ?ResponsePaginationInterface
    {
        return new PagedResponsePagination(
            $this->currentPage,
            $this->totalPages,
            $this->total,
        );
    }

    public function hasNextPage(): bool
    {
        return $this->currentPage < $this->totalPages;
    }

    public function hasPreviousPage(): bool
    {
        return $this->currentPage > 1;
    }
}
```

Advanced Examples
-----------------

[](#advanced-examples)

### Handling File Uploads

[](#handling-file-uploads)

```
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;
use Wimski\HttpRequests\Factories\AbstractStreamFactory;

readonly class MultipartStreamFactory extends AbstractStreamFactory
{
    public function supports(string $contentType): bool
    {
        return str_starts_with($contentType, 'multipart/form-data');
    }

    public function make(RequestInterface $request): string
    {
        $boundary = uniqid('', true);
        $body = '';

        foreach ($request->getBody() as $name => $value) {
            $body .= "--{$boundary}\r\n";
            $body .= "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n";
            $body .= "{$value}\r\n";
        }

        $body .= "--{$boundary}--\r\n";

        return $body;
    }
}
```

### Rate Limiting

[](#rate-limiting)

```
use Psr\Http\Message\RequestInterface as HttpRequestInterface;
use Psr\Http\Message\ResponseInterface as HttpResponseInterface;
use Wimski\HttpRequests\Client;
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;

readonly class RateLimitedClient extends Client
{
    protected int $lastRequestTime = 0;
    protected int $minDelayMs = 100;

    protected function sendRequest(
        HttpRequestInterface $httpRequest,
        RequestInterface $request
    ): HttpResponseInterface {
        $now = (int) (microtime(true) * 1000);
        $elapsed = $now - $this->lastRequestTime;

        if ($elapsed < $this->minDelayMs) {
            usleep(($this->minDelayMs - $elapsed) * 1000);
        }

        $response = parent::sendRequest($httpRequest, $request);
        $this->lastRequestTime = (int) (microtime(true) * 1000);

        return $response;
    }
}
```

### Retry Logic

[](#retry-logic)

```
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface as HttpRequestInterface;
use Psr\Http\Message\ResponseInterface as HttpResponseInterface;
use Wimski\HttpRequests\Client;
use Wimski\HttpRequests\Contracts\Requests\RequestInterface;

readonly class RetryableClient extends Client
{
    public function __construct(
        protected int $maxRetries = 3,
        protected int $retryDelayMs = 1000,
        // ... parent dependencies
    ) {
        parent::__construct(...);
    }

    protected function sendRequest(
        HttpRequestInterface $httpRequest,
        RequestInterface $request
    ): HttpResponseInterface {
        $attempt = 0;
        $lastException = null;

        while ($attempt < $this->maxRetries) {
            try {
                return parent::sendRequest($httpRequest, $request);
            } catch (ClientExceptionInterface $exception) {
                $lastException = $exception;
                $attempt++;

                if ($attempt < $this->maxRetries) {
                    usleep($this->retryDelayMs * 1000 * $attempt);
                }
            }
        }

        throw $lastException;
    }
}
```

Exception Handling
------------------

[](#exception-handling)

The library provides specific exceptions for different failure scenarios:

```
use Wimski\HttpRequests\Exceptions\HttpStatusCodeException;
use Wimski\HttpRequests\Exceptions\RequestException;
use Wimski\HttpRequests\Exceptions\ResponseException;

try {
    $response = $client->request($request);
} catch (HttpStatusCodeException $exception) {
    // HTTP error status code (4xx, 5xx)
    $statusCode = $exception->getResponse()->getStatusCode();
    $body = $exception->getMessage();
} catch (RequestException $exception) {
    // Failed to create or send the request
    $previous = $exception->getPrevious();
} catch (ResponseException $e) {
    // Failed to parse or transform the response
    $previous = $exception->getPrevious();
}
```

Testing
-------

[](#testing)

The interface-driven design makes testing straightforward:

```
use Mockery;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Wimski\HttpRequests\Contracts\ClientInterface;

class UserServiceTest extends TestCase
{
    #[Test]
    public function test_can_fetch_user(): void
    {
        $client = Mockery::mock(ClientInterface::class);

        $client
            ->shouldReceive('request')
            ->once()
            ->andReturn(new SingleResponse(new User(1, 'John', 'john@example.com')));

        $service = new UserService($client);

        $user = $service->getUser(1);

        $this->assertEquals('John', $user->name);
    }
}
```

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance87

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~25 days

Total

2

Last Release

65d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/12373573?v=4)[Wim Reckman](/maintainers/wimski)[@wimski](https://github.com/wimski)

---

Top Contributors

[![wimski](https://avatars.githubusercontent.com/u/12373573?v=4)](https://github.com/wimski "wimski (14 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/wimski-http-requests/health.svg)

```
[![Health](https://phpackages.com/badges/wimski-http-requests/health.svg)](https://phpackages.com/packages/wimski-http-requests)
```

###  Alternatives

[guzzlehttp/psr7

PSR-7 message implementation that also provides common utility methods

8.0k1.1B3.9k](/packages/guzzlehttp-psr7)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k13](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[cakephp/cakephp

The CakePHP framework

8.9k19.5M1.8k](/packages/cakephp-cakephp)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

35789.4k2](/packages/telnyx-telnyx-php)[laudis/neo4j-php-client

Neo4j-PHP-Client is the most advanced PHP Client for Neo4j

185702.8k42](/packages/laudis-neo4j-php-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
