PHP code example of zlodes / http-client

1. Go to this page and download the library: Download zlodes/http-client library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

zlodes / http-client example snippets


use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
use Zlodes\Http\Client\Contract\Request as RequestContract;

/**
 * @implements RequestContract<GetUserResponse>
 */
final readonly class GetUserRequest implements RequestContract
{
    public function __construct(private int $userId) {}

    public function getName(): string
    {
        return 'users.get';
    }

    public function buildRequest(): RequestInterface
    {
        return new Request(
            method: 'GET',
            uri: "https://api.example.com/users/{$this->userId}",
        );
    }

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

use Zlodes\Http\Client\Contract\Response;

final readonly class GetUserResponse implements Response
{
    public function __construct(
        public int $id,
        public string $name,
    ) {}
}

use Psr\Http\Message\ResponseInterface;
use Zlodes\Http\Client\Contract\Request;
use Zlodes\Http\Client\Contract\Response;
use Zlodes\Http\Client\Contract\ResponseHydrator;

final readonly class JsonHydrator implements ResponseHydrator
{
    public function __construct(private SerializerInterface $serializer) {}

    public function hydrate(ResponseInterface $response, Request $request): Response
    {
        return $this->serializer->deserialize(
            data: (string) $response->getBody(),
            type: $request->getResponseClass(),
            format: 'json',
        );
    }
}

use Zlodes\Http\Client\HttpClient;
use Zlodes\Http\Client\Transport\Psr18Transport;

$transport = new Psr18Transport(client: $psr18Client);

$client = new HttpClient(
    transport: $transport,
    responseHydrator: $responseHydrator,
);

// PHPStan knows this returns GetUserResponse
$response = $client->send(
    request: new GetUserRequest(userId: 42),
);
echo $response->name;

use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
use Zlodes\Http\Client\Contract\Request as RequestContract;
use Zlodes\Http\Client\EmptyResponse;

/**
 * @implements RequestContract<EmptyResponse>
 */
final readonly class DeleteUserRequest implements RequestContract
{
    // ...

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

$client->send(request: new DeleteUserRequest(userId: 42));

use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
use Zlodes\Http\Client\Contract\CollectionRequest;

/**
 * @implements CollectionRequest<UserDto>
 */
final readonly class ListUsersRequest implements CollectionRequest
{
    public function getName(): string
    {
        return 'users.list';
    }

    public function buildRequest(): RequestInterface
    {
        return new Request(method: 'GET', uri: 'https://api.example.com/users');
    }

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

    public function getItemClass(): string
    {
        return UserDto::class;
    }
}

use Psr\Http\Message\ResponseInterface;
use Zlodes\Http\Client\CollectionResponse;
use Zlodes\Http\Client\Contract\CollectionRequest;
use Zlodes\Http\Client\Contract\Request;
use Zlodes\Http\Client\Contract\Response;
use Zlodes\Http\Client\Contract\ResponseHydrator;

final readonly class JsonHydrator implements ResponseHydrator
{
    public function __construct(private SerializerInterface $serializer) {}

    public function hydrate(ResponseInterface $response, Request $request): Response
    {
        $body = (string) $response->getBody();

        if ($request instanceof CollectionRequest) {
            $items = $this->serializer->deserialize(
                data: $body,
                type: $request->getItemClass() . '[]',
                format: 'json',
            );

            return CollectionResponse::fromHydrated(request: $request, items: $items);
        }

        return $this->serializer->deserialize(
            data: $body,
            type: $request->getResponseClass(),
            format: 'json',
        );
    }
}

use Zlodes\Http\Client\Contract\HasResponseHydrator;
use Zlodes\Http\Client\Contract\ResponseHydrator;

final readonly class LegacyApiRequest implements RequestContract, HasResponseHydrator
{
    public function __construct(
        private string $id,
        private ResponseHydrator $hydrator,
    ) {}

    public function getResponseHydrator(): ResponseHydrator
    {
        return $this->hydrator;
    }

    // ... getName(), buildRequest(), getResponseClass()
}

use Psr\Http\Message\ResponseInterface;
use Zlodes\Http\Client\Contract\ErrorResponseHandler;
use Zlodes\Http\Client\Contract\Request as RequestContract;
use Zlodes\Http\Client\Exception\HttpClientException;
use Zlodes\Http\Client\Exception\HttpErrorException;

final class ValidationFailedException extends HttpErrorException
{
    public function __construct(
        public readonly array $errors,
        RequestContract $request,
        ResponseInterface $response,
    ) {
        parent::__construct(
            request: $request,
            response: $response,
            payload: $errors,
            message: 'Validation failed',
        );
    }
}

final readonly class ValidationErrorHandler implements ErrorResponseHandler
{
    public function supports(ResponseInterface $response, RequestContract $request): bool
    {
        return $response->getStatusCode() === 422
            && $request instanceof CreateUserRequest;
    }

    public function toException(ResponseInterface $response, RequestContract $request): HttpClientException
    {
        /** @var array{errors: array<string, list<string>>} $payload */
        $payload = json_decode(
            json: (string) $response->getBody(),
            associative: true,
            flags: JSON_THROW_ON_ERROR,
        );

        return new ValidationFailedException(
            errors: $payload['errors'],
            request: $request,
            response: $response,
        );
    }
}

$client = new HttpClient(
    transport: $transport,
    responseHydrator: $responseHydrator,
    middlewares: [],
    errorResponseHandlers: [new ValidationErrorHandler()],
);

$request = new CreateUserRequest(input: $input);

try {
    $response = $client->send(request: $request);
} catch (ValidationFailedException $e) {
    return CreateUserResult::validationFailed(errors: $e->errors);
} catch (HttpErrorException $e) {
    // Fallback for unhandled upstream errors. Raw PSR-7 response is available on $e->response.
}

use Zlodes\Http\Client\Factory\Option\WithTimeout;

// Global default, applied to every client from this factory
$factory = new ClientFactory(defaults: [
    new WithTransport($transport),
    new WithResponseHydrator($hydrator),
    new WithTimeout(timeout: 10, connectTimeout: 3),
]);

// Per client: overrides only the values it sets
$slowClient = $factory->make(
    new WithTimeout(timeout: 60),
);

use Zlodes\Http\Client\Contract\HasTransportOptions;
use Zlodes\Http\Client\TransportOptions;

final readonly class SlowReportRequest implements RequestContract, HasTransportOptions
{
    public function getTransportOptions(): TransportOptions
    {
        return new TransportOptions(timeout: 120);
    }

    // ... getName(), buildRequest(), getResponseClass()
}

use Zlodes\Http\Client\Middleware\TimeoutMiddleware;

$client = new HttpClient(
    transport: $transport,
    responseHydrator: $responseHydrator,
    middlewares: [new TimeoutMiddleware(timeout: 10, connectTimeout: 3)],
);

use Zlodes\Http\Client\Factory\Option\WithHeader;

$factory = new ClientFactory(defaults: [
    new WithTransport($transport),
    new WithResponseHydrator($hydrator),
    new WithHeader('User-Agent', 'B2CORE Monolith/22.45.0 (tenant: acme)'),
    new WithHeader('X-Tenant', 'acme'),
]);

use Zlodes\Http\Client\Middleware\HeaderMiddleware;

$client = new HttpClient(
    transport: $transport,
    responseHydrator: $responseHydrator,
    middlewares: [
        new HeaderMiddleware('User-Agent', 'B2CORE Monolith/22.45.0'),
    ],
);

use Zlodes\Http\Client\Contract\Middleware;
use Zlodes\Http\Client\Contract\RequestHandler;
use Zlodes\Http\Client\RequestContext;
use Psr\Http\Message\ResponseInterface;

final readonly class AuthMiddleware implements Middleware
{
    public function process(RequestContext $context, RequestHandler $next): ResponseInterface
    {
        $authenticatedRequest = $context->httpRequest->withHeader(
            name: 'Authorization',
            value: 'Bearer ...',
        );

        return $next->handle(
            context: $context->withHttpRequest(httpRequest: $authenticatedRequest),
        );
    }
}

use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Zlodes\Http\Client\Contract\Middleware;
use Zlodes\Http\Client\Contract\RequestHandler;
use Zlodes\Http\Client\RequestContext;

final readonly class LoggingMiddleware implements Middleware
{
    public function __construct(private LoggerInterface $logger) {}

    public function process(RequestContext $context, RequestHandler $next): ResponseInterface
    {
        $this->logger->info(
            message: 'Sending HTTP request',
            context: [
                'name' => $context->requestName,
                'method' => $context->httpRequest->getMethod(),
                'uri' => (string) $context->httpRequest->getUri(),
            ],
        );

        try {
            $response = $next->handle(context: $context);
        } catch (\Throwable $e) {
            $this->logger->error(
                message: 'HTTP request failed',
                context: [
                    'name' => $context->requestName,
                    'method' => $context->httpRequest->getMethod(),
                    'uri' => (string) $context->httpRequest->getUri(),
                    'error' => $e->getMessage(),
                ],
            );

            throw $e;
        }

        $this->logger->info(
            message: 'HTTP response received',
            context: [
                'name' => $context->requestName,
                'status' => $response->getStatusCode(),
            ],
        );

        return $response;
    }
}

use Psr\Http\Message\ResponseInterface;
use Zlodes\Http\Client\Contract\Middleware;
use Zlodes\Http\Client\Contract\RequestHandler;
use Zlodes\Http\Client\RequestContext;

interface MetricsCollector
{
    public function recordRequest(string $name, string $method, int $statusCode, float $duration): void;
}

final readonly class MetricsMiddleware implements Middleware
{
    public function __construct(private MetricsCollector $collector) {}

    public function process(RequestContext $context, RequestHandler $next): ResponseInterface
    {
        $start = hrtime(as_number: true);
        $response = $next->handle(context: $context);
        $duration = (hrtime(as_number: true) - $start) / 1e9;

        $this->collector->recordRequest(
            name: $context->requestName,
            method: $context->httpRequest->getMethod(),
            statusCode: $response->getStatusCode(),
            duration: $duration,
        );

        return $response;
    }
}

$client = new HttpClient(
    transport: $transport,
    responseHydrator: $responseHydrator,
    middlewares: [
        new LoggingMiddleware(logger: $logger),
        new MetricsMiddleware(collector: $metricsCollector),
    ],
);

use Zlodes\Http\Client\Factory\ClientFactory;
use Zlodes\Http\Client\Factory\Option\WithTransport;
use Zlodes\Http\Client\Factory\Option\WithResponseHydrator;
use Zlodes\Http\Client\Factory\Option\WithMiddleware;
use Zlodes\Http\Client\Factory\Option\WithBaseUri;

// Shared defaults for all clients
$factory = new ClientFactory(defaults: [
    new WithTransport($transport),
    new WithResponseHydrator($hydrator),
    new WithMiddleware($loggingMiddleware, $metricsMiddleware),
]);

// Per-service clients add their own options
$usersClient = $factory->make(
    new WithBaseUri(new Uri('https://users-api.example.com')),
);

$billingClient = $factory->make(
    new WithBaseUri(new Uri('https://billing-api.example.com')),
    new WithMiddleware($billingAuthMiddleware),
);

$factory = new ClientFactory(
    defaults: $defaults,
    clientCreator: function (HttpClientConfig $config): HttpClient {
        // Custom validation, decoration, etc.
        return new HttpClient(
            $config->transport,
            $config->responseHydrator,
            $config->middlewares,
            $config->errorResponseHandlers,
            $config->baseUri,
        );
    },
);