Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Httpclient Laravel Package

payment/httpclient

payment/httpclient is a Laravel package for making HTTP requests to payment providers. It wraps outbound payment API calls with configurable request handling and response parsing to simplify gateway integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

payment/httpclient is a thin implementation of the (now abandoned) PHP-FIG HTTP client proposal that beberlei/fig-standards was drafting in 2013–2014. Practically, it gives you an interface with a small request/response API and a pluggable transport — the value is the seam it creates, not the transport itself.

Install and pin. The package is archived and last tagged in October 2014, so treat it as vendored code, not a dependency you can float:

composer require payment/httpclient:^1.0

Then immediately check what you actually got:

cat vendor/payment/httpclient/composer.json   # autoload namespace + PHP constraint
ls vendor/payment/httpclient/src              # interface names, transport implementations

Where to look first. In order:

  1. The main interface (namespace is Payment\HttpClient per the package name — confirm against the autoload block, don't assume). This defines createRequest() / send() and whatever convenience helpers exist.
  2. The request and response value objects — specifically whether the response exposes status, headers, and body as methods or public properties, and whether the body is a string or a stream.
  3. The transport implementations, especially any mock/fake one. That is the reason to use this package at all.
  4. The cURL transport, to see what timeouts, TLS options, and redirect handling are hardcoded. In a 2014 codebase these are usually defaults you do not want.

First use case. Wrap a single payment gateway call behind the interface and bind it in a service provider, so application code never touches curl or Guzzle:

// app/Providers/PaymentsServiceProvider.php
public function register(): void
{
    $this->app->singleton(HttpClientInterface::class, function ($app) {
        $config = $app['config']['services.acme_pay'];

        $client = new HttpClient(new CurlTransport());
        $client->setBaseUrl($config['base_url']);   // use whatever the package actually exposes
        $client->setTimeout($config['timeout'], $config['connect_timeout']);

        return new LoggingHttpClient($client);      // decorator, see below
    });
}

Then inject the interface into a thin gateway class. Keep the first milestone small: one POST /charges round-trip, one domain object out, one test against the fake transport. Once that path is proven, extend to refunds and status lookups.

Implementation Patterns

Inject the interface, never the concrete client. Every payment gateway class should take HttpClientInterface in its constructor. This is the only thing that makes swapping to PSR-18/Guzzle later a mechanical change instead of a rewrite.

Build a per-gateway adapter, and map at the boundary. The package's response object is a transport concern. Convert it to your own result type immediately:

final class AcmePayGateway
{
    public function __construct(private HttpClientInterface $http) {}

    public function charge(Money $amount, string $token): ChargeResult
    {
        $request = $this->http->createRequest('POST', '/v1/charges');
        $request->setHeader('Authorization', 'Bearer ' . config('services.acme_pay.key'));
        $request->setHeader('Idempotency-Key', $idempotencyKey);  // generate upstream, in the job
        $request->setBody(json_encode([...], JSON_THROW_ON_ERROR));

        $response = $this->http->send($request);
        $payload  = json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR);

        return ChargeResult::fromPayload($response->getStatusCode(), $payload);
    }
}

Nothing above the gateway should know that payment/httpclient exists.

Decorator stack for cross-cutting concerns. Because the package defines an interface, Laravel-idiomatic "middleware" is just a chain of implementations. Compose in the service provider:

interface HttpClientInterface { /* from the package */ }

final class LoggingHttpClient implements HttpClientInterface
{
    public function __construct(private HttpClientInterface $inner) {}

    public function send($request)
    {
        $start = microtime(true);
        try {
            $response = $this->inner->send($request);
            Log::info('http', [
                'method' => $request->getMethod(),
                'url'    => $request->getUrl(),
                'status' => $response->getStatusCode(),
                'ms'     => (int) ((microtime(true) - $start) * 1000),
            ]);
            return $response;
        } catch (\Throwable $e) {
            Log::warning('http.failed', ['url' => $request->getUrl(), 'error' => $e->getMessage()]);
            throw $e;
        }
    }

    // delegate every other method to $this->inner
}

Use the same shape for metrics, a circuit breaker, and header injection. Keep the stack to two or three layers; each one is another place a signature mismatch can hide.

Retries belong in the queue, not the client. This package has no retry, backoff, or jitter logic — and it shouldn't. Dispatch payment calls as jobs with $tries, $backoff, and a hard idempotency key generated before dispatch and stored on the job, so a redelivered job cannot double-charge.

Testing: swap the transport, not the network. The whole point of the pluggable transport is that tests never leave the process:

public function test_charge_maps_declined_response(): void
{
    $fake = new FakeTransport();                 // or whatever the package ships
    $fake->queueResponse(402, json_encode(['error' => 'card_declined']));

    $this->app->instance(HttpClientInterface::class, new HttpClient($fake));

    $result = $this->app->make(AcmePayGateway::class)
        ->charge(Money::usd(1000), 'tok_visa');

    $this->assertTrue($result->declined());
    $this->assertSame('card_declined', $result->reason());
}

If the shipped fake is too thin (many 2014 fakes only return a canned 200), write a 30-line RecordingTransport that captures the last request and returns a queued response. That recorder doubles as your assertion surface for headers and bodies.

Handle inbound webhooks separately. This package is outbound-only. Gateway callbacks are inbound HTTP — route them through Laravel controllers, verify signatures yourself, and don't try to force them through this client.

Adopt an anti-corruption adapter from day one. Even while you're using the package, define your own PaymentHttpClient interface in app/, implement it with the package, and inject that. Six months from now the swap is one class:

final class Psr18PaymentHttpClient implements PaymentHttpClient
{
    public function __construct(private ClientInterface $client) {}

    public function post(string $path, array $headers, string $body): PaymentResponse
    {
        $response = $this->client->sendRequest(
            new Request('POST', $path, $headers, $body)
        );
        return new PaymentResponse($response->getStatusCode(), (string) $response->getBody());
    }
}

Gotchas and Tips

It is archived, and the spec it implements died. The FIG HTTP client proposal was superseded by PSR-7/PSR-18 and HTTPlug. Nothing here interoperates with PSR-7 messages, PSR-18 clients, Guzzle middleware, or Laravel's Http facade. Treat the dependency as a liability with a scheduled exit, not a foundation. Fork it into your repo (repositories: [{type: path}]) rather than relying on Packagist availability.

Check the PHP compatibility before anything else. Code from 2014 can hit removed features on modern PHP: create_function(), each(), ${var} string interpolation (removed in 8.2), dynamic properties (deprecated in 8.2), and untyped signatures that clash with your strict types. Run php -l across the files and add the package to your PHPStan baseline rather than fighting it.

Audit the cURL defaults. This is the highest-risk part of a payment integration. Verify explicitly that:

  • CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are enabled — 2014-era code frequently disabled them for convenience.
  • TLS 1.2+ is negotiated; old defaults may attempt TLS 1.0 against gateways that now reject it.
  • Connect and total timeouts are non-zero and sane (e.g. 2s / 10s). A zero timeout blocks a queue worker indefinitely.
  • Redirects are off for payment POSTs — a 302 on a charge request should be an error, not a silent replay.
  • CURLOPT_PROXY isn't inherited from the environment unexpectedly.

Error semantics are ambiguous — normalize them. Old HTTP clients vary wildly: some throw on 4xx, some throw only on transport failure, some return a response with status 0 when DNS fails. Find out which, then wrap send() in one place and convert to two exceptions: PaymentTransportException (retryable) and PaymentGatewayException (not retryable, contains the gateway error code). Never let a status-0 response reach business logic as if it were a real HTTP response.

Response bodies may be read-once or fully buffered. If the response wraps a stream, getBody() might be consumed after the first call — cache the decoded payload in your adapter. Conversely, if it buffers everything into a string, a large or hostile response can exhaust memory; there is no size guard in a package this old.

URL construction is manual. Expect no URI template or query builder. Build paths with sprintf/concatenation and use http_build_query() for query strings — double-encoding and missing slash-joins are the classic bugs here, and the failure mode is a 404 that looks like a gateway outage.

Headers are probably case-sensitive and string-based. Don't pass arrays where strings are expected, and check whether the client merges or replaces duplicate headers. If you inject Authorization via a decorator and in the gateway, you may end up with two headers — some gateways reject that rather than ignoring the duplicate.

Never log request bodies. Payment payloads contain PANs, CVVs, and tokens. The logging decorator above deliberately logs only method, URL, status, and duration. If you need body logging for debugging, redact by key allowlist, not blocklist, and gate it behind a config flag that is off in production.

Watch for stray real calls in tests. If a test forgets to bind the fake transport, you get a live request against the sandbox — or worse, production. Guard with a base test-case setUp() that asserts the container is bound to the fake, and set the gateway base URL to http://127.0.0.1:1 in phpunit.xml so an unbound client fails instantly.

Extension point: implement the interface yourself. The cleanest escape hatch is to skip the package's transport entirely and provide your own HttpClientInterface implementation backed by Guzzle, with MockHandler for tests. You keep the interface contract your code already depends on and drop the 2014 cURL code the same afternoon. Do this as soon as you have a second gateway to integrate — by then the abstraction has paid for itself and the archive risk starts to dominate.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource