flikson/laravel-service-client

HTTP client package for Laravel with retry, multi-host, circuit breaker, and async support

Maintainers

Package info

github.com/fliksonr/laravel-service-client

pkg:composer/flikson/laravel-service-client

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

1.0.1 2026-04-04 14:04 UTC

This package is auto-updated.

Last update: 2026-05-04 14:20:25 UTC


README

🌐 Language: English | Русский

Laravel-focused HTTP client with retry, multi-host failover, circuit breaker, and async support via PHP Fibers.

Features

  • Retry with exponential backoff β€” configurable delay, jitter, and status codes
  • Multi-host strategies β€” failover, round-robin, parallel
  • Circuit breaker β€” automatic failure detection and recovery
  • Async via Fibers β€” non-blocking requests with Revolt event loop
  • Middleware pipeline β€” extensible request/response processing
  • Multiple auth methods β€” Bearer, API Key, Basic
  • Typed requests β€” ISP-based interfaces with response mapping

Requirements

  • PHP 8.4+
  • Guzzle 7.9+
  • Illuminate Support/Contracts 11+ | 12+

Installation

composer require flikson/laravel-service-client

Laravel

Laravel package discovery is supported via extra.laravel.providers.

Publish config:

php artisan vendor:publish --tag=deeep-service-client-config

The published config file path:

config/deeep_service_client.php

If package discovery is disabled, register provider manually in config/app.php:

'providers' => [
    // ...
    Deeep\ServiceClient\Laravel\ServiceClientServiceProvider::class,
],

Resolve client from container:

use Deeep\ServiceClient\Contract\HttpClientInterface;

$client = app(HttpClientInterface::class);

Configuration

<?php
// config/deeep_service_client.php

return [
    'defaults' => [
        'timeout' => [
            'connect' => 5.0,
            'request' => 30.0,
        ],
        'retry' => [
            'enabled' => true,
            'max_attempts' => 3,
            'delay' => 1000,
            'multiplier' => 2.0,
            'jitter' => 0.1,
        ],
    ],
    'services' => [
        'my_api' => [
            'host' => env('MY_API_HOST'),
            'auth' => [
                'type' => 'bearer',
                'credentials' => [
                    'token' => env('MY_API_TOKEN'),
                ],
            ],
        ],
        'resilient_api' => [
            'hosts' => [
                'strategy' => 'failover',
                'list' => [
                    ['url' => env('API_PRIMARY')],
                    ['url' => env('API_BACKUP')],
                ],
            ],
        ],
    ],
];

Usage

Typed Request (Recommended)

use Deeep\ServiceClient\Contract\AuthRequiredInterface;
use Deeep\ServiceClient\Contract\MappableRequestInterface;
use Deeep\ServiceClient\Contract\ResponseInterface;
use Deeep\ServiceClient\Enum\HttpMethod;
use Deeep\ServiceClient\Request\AbstractRequest;

/**
 * @implements MappableRequestInterface<User>
 */
final readonly class GetUserRequest extends AbstractRequest implements
    AuthRequiredInterface,
    MappableRequestInterface
{
    public function __construct(
        private int $userId,
    ) {}

    public function getService(): string
    {
        return 'my_api';
    }

    public function getMethod(): HttpMethod
    {
        return HttpMethod::GET;
    }

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

    /**
     * @param ResponseInterface<mixed> $response
     */
    public function mapResponse(ResponseInterface $response): User
    {
        $data = $response->toArray();

        return new User($data['id'], $data['name'], $data['email']);
    }
}

// Usage
$request = new GetUserRequest(123);
$httpClient = app(HttpClient::class);
$response = $httpClient->send($request);
$user = $response->getData(); // User

POST Request with Body

use Deeep\ServiceClient\Contract\AuthRequiredInterface;
use Deeep\ServiceClient\Contract\MappableRequestInterface;
use Deeep\ServiceClient\Contract\ResponseInterface;
use Deeep\ServiceClient\Enum\HttpMethod;
use Deeep\ServiceClient\Request\AbstractRequest;

/**
 * @implements MappableRequestInterface<User>
 */
final readonly class CreateUserRequest extends AbstractRequest implements
    AuthRequiredInterface,
    MappableRequestInterface
{
    public function __construct(
        private string $name,
        private string $email,
    ) {}

    public function getService(): string
    {
        return 'my_api';
    }

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

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

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

    /**
     * @param ResponseInterface<mixed> $response
     */
    public function mapResponse(ResponseInterface $response): User
    {
        $data = $response->toArray();

        return new User($data['id'], $data['name'], $data['email']);
    }
}

Request Builder (Quick Requests)

$request = $requestBuilder
    ->service('my_api')
    ->get('/users')
    ->query(['limit' => 10])
    ->build();

$httpClient = app(HttpClient::class);
$response = $httpClient->send($request);
$users = $response->toArray();

The builder is immutable β€” each method returns a new instance:

$base = $requestBuilder->service('api')->header('X-Tenant', 'acme');
$request1 = $base->get('/users')->build();
$request2 = $base->get('/orders')->build();  // Both have X-Tenant header

Async Requests

// Launch multiple requests
$results = [];
foreach ($userIds as $id) {
    $results[$id] = $httpClient->sendAsync(new GetUserRequest($id));
}

// Await all
$responses = $httpClient->await(...$results);

foreach ($responses as $id => $response) {
    $users[$id] = $response->getData();
}

Callbacks

$httpClient->sendAsync(
    $request,
    onResponse: fn($response) => $this->process($response),
    onError: fn($e) => $this->logger->error($e->getMessage()),
);

Error Response Callbacks

Use onErrorResponse() for handling 4xx/5xx responses separately from network errors:

$result = $httpClient->sendAsync($request);

$result
    ->then(fn($response) => $this->handleSuccess($response))
    ->onErrorResponse(fn($response) => $this->handleApiError($response))
    ->then(null, fn($e) => $this->handleNetworkError($e));

$response = $result->await();
  • then($onSuccess) β€” called for 2xx responses
  • onErrorResponse($callback) β€” called for 4xx/5xx responses with body
  • then(null, $onError) β€” called for network errors (timeout, connection failed)

Request Interfaces (ISP)

Interface Purpose
RequestInterface Base request (service, method, uri, headers, query, body)
AuthRequiredInterface Marker: request requires authentication
ConfigurableRequestInterface Override timeout/retry for specific request
MappableRequestInterface<T> Custom response mapping to type T
ErrorMappableRequestInterface<E> Custom error response (4xx/5xx) mapping to type E

Also available: BaseRequest β€” base class with default implementations for getHeaders(), getQuery(), getBody().

Handling Error Responses

Implement ErrorMappableRequestInterface for custom mapping of error responses (4xx/5xx):

use Deeep\ServiceClient\Contract\ErrorMappableRequestInterface;
use Deeep\ServiceClient\Contract\MappableRequestInterface;
use Deeep\ServiceClient\Contract\ResponseInterface;
use Deeep\ServiceClient\Request\AbstractRequest;

/**
 * @implements MappableRequestInterface<User>
 * @implements ErrorMappableRequestInterface<ApiError>
 */
final readonly class GetUserRequest extends AbstractRequest implements
    MappableRequestInterface,
    ErrorMappableRequestInterface
{
    // ... getService(), getMethod(), getUri()

    /**
     * @param ResponseInterface<mixed> $response
     */
    public function mapResponse(ResponseInterface $response): User
    {
        $data = $response->toArray();

        return new User($data['id'], $data['name'], $data['email']);
    }

    /**
     * @param ResponseInterface<mixed> $response
     */
    public function mapErrorResponse(ResponseInterface $response): ApiError
    {
        $data = $response->toArray();

        return new ApiError($data['code'], $data['message']);
    }
}

Multi-Host Strategies

Failover

Tries hosts in order, switches on 5xx or network error:

'services' => [
    'api' => [
        'hosts' => [
            'strategy' => 'failover',
            'list' => [
                ['url' => 'https://primary.example.com'],
                ['url' => 'https://backup.example.com'],
            ],
        ],
    ],
],

Round-Robin

Distributes load across hosts:

'services' => [
    'api' => [
        'hosts' => [
            'strategy' => 'round_robin',
            'list' => [
                ['url' => 'https://node1.example.com'],
                ['url' => 'https://node2.example.com'],
                ['url' => 'https://node3.example.com'],
            ],
        ],
    ],
],

Parallel

Sends to all hosts, returns first successful response:

'services' => [
    'api' => [
        'hosts' => [
            'strategy' => 'parallel',
            'list' => [
                ['url' => 'https://fast.example.com'],
                ['url' => 'https://slow.example.com'],
            ],
        ],
    ],
],

Retry Configuration

'defaults' => [
    'retry' => [
        'enabled' => true,
        'max_attempts' => 3,           // Total attempts (1 initial + 2 retries)
        'delay' => 1000,               // Initial delay in ms
        'multiplier' => 2.0,           // Exponential multiplier
        'max_delay' => 30000,          // Maximum delay in ms
        'jitter' => 0.1,               // Random jitter (0-1)
        'retry_on' => [429, 502, 503, 504],
        'retry_on_methods' => ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS'],
        'respect_retry_after' => true,
    ],
],

Authentication Types

Bearer Token

'services' => [
    'api' => [
        'host' => '%env(resolve:API_HOST)%',
        'auth' => [
            'type' => 'bearer',
            'credentials' => [
                'token' => '%env(resolve:API_TOKEN)%',
            ],
        ],
    ],
],

API Key

'services' => [
    'api' => [
        'host' => '%env(resolve:API_HOST)%',
        'auth' => [
            'type' => 'api_key',
            'credentials' => [
                'header' => 'X-Api-Key',  // Default: Api-Key
                'token' => '%env(resolve:API_KEY)%',
            ],
        ],
    ],
],

Basic Auth

'services' => [
    'api' => [
        'host' => '%env(resolve:API_HOST)%',
        'auth' => [
            'type' => 'basic',
            'credentials' => [
                'username' => '%env(resolve:API_USER)%',
                'password' => '%env(resolve:API_PASS)%',
            ],
        ],
    ],
],

Middleware Pipeline

Requests pass through middleware chain in priority order:

Request β†’ Auth(100) β†’ Retry(90) β†’ Logging(80) β†’ CircuitBreaker(60) β†’ MultiHost(10) β†’ Transport
Priority Middleware Purpose
100 AuthMiddleware Adds Authorization header
90 RetryMiddleware Retries with exponential backoff
80 LoggingMiddleware Logs requests/responses
60 CircuitBreakerMiddleware Cascade failure protection
10 MultiHostMiddleware Failover/round-robin/parallel

More details: docs/en/middleware.md

Documentation

License

BSD-3-Clause