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

App Payment Client Laravel Package

s1ro/app-payment-client

A Laravel/PHP client for integrating application payments, offering a simple way to connect your app to a payment service and manage payment-related operations from your codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect before you write code.

composer require s1ro/app-payment-client

The package has effectively zero public adoption (no stars, no dependents), so treat its README as a starting hypothesis rather than documentation. Spend the first 10 minutes reading the source:

  1. composer.json — PHP and illuminate/* constraints. This tells you immediately whether it will even install on your Laravel version, and what the PSR-4 root namespace is.
  2. src/ — find the client entry class (usually AppPaymentClient, Client, or PaymentClient) and the service provider. The provider reveals what is bound into the container and whether a config file is published.
  3. config/*.php — the real configuration contract: base URL, credentials, timeout, retry settings. This is the authoritative list, not the README.
  4. Any Exceptions/ directory — that's the error contract you'll build your handling around.

Publish and configure.

php artisan vendor:publish --provider="S1ro\AppPaymentClient\AppPaymentClientServiceProvider"
APP_PAYMENT_BASE_URL=https://payments.example.com
APP_PAYMENT_CLIENT_ID=
APP_PAYMENT_CLIENT_SECRET=
APP_PAYMENT_TIMEOUT=10

First use case: create a payment and read it back. Resolve the client from the container (don't new it — you'll lose the configured HTTP client and credentials):

use S1ro\AppPaymentClient\AppPaymentClient;

$client = app(AppPaymentClient::class);

$payment = $client->payments()->create([
    'amount'      => 2599,          // minor units — verify with the source
    'currency'    => 'EUR',
    'reference'   => $order->uuid,
    'idempotency_key' => (string) Str::uuid(),
]);

$status = $client->payments()->find($payment->id());

Method names above are illustrative — confirm each one against src/ before shipping. If the client exposes a __call passthrough or a request() escape hatch, note it; you'll need it for endpoints the wrapper hasn't covered.

Implementation Patterns

Never call the package directly from controllers. Put a thin adapter between your domain and the SDK. When the package is abandoned or the API changes, you rewrite one class instead of forty call sites.

interface PaymentGateway
{
    public function charge(Order $order): PaymentResult;
    public function status(string $paymentId): PaymentStatus;
}

final class AppPaymentGateway implements PaymentGateway
{
    public function __construct(private AppPaymentClient $client) {}

    public function charge(Order $order): PaymentResult
    {
        $response = $this->client->payments()->create([
            'amount'          => $order->total_minor,
            'currency'        => $order->currency,
            'reference'       => $order->uuid,
            'idempotency_key' => "order-{$order->id}",
        ]);

        return PaymentResult::fromArray($response->toArray());
    }
}

Bind the implementation in your own AppServiceProvider, and bind the SDK itself as a singleton so connection settings and any internal token cache are reused across the request:

$this->app->singleton(AppPaymentClient::class, fn ($app) => new AppPaymentClient(
    config('app-payment.base_url'),
    config('app-payment.client_id'),
    config('app-payment.client_secret'),
));

Only do the manual construction if the package's own provider resolves configuration at boot in a way you can't override — check first.

Wrap mutations in a queued job with idempotency. Payment calls are the classic "the network timed out but the charge went through" problem. Persist an idempotency key before dispatching, and let the retry reuse it:

final class ChargeOrder implements ShouldQueue
{
    public int $tries = 5;
    public array $backoff = [5, 15, 60, 180];

    public function handle(AppPaymentGateway $gateway): void
    {
        $result = $gateway->charge($this->order);

        $this->order->update([
            'payment_id'     => $result->id,
            'payment_status' => $result->status,
        ]);
    }
}

Consume webhooks in a separate, verified path. If the package ships signature verification, use it; if not, implement it yourself against the raw body. Always respond 200 fast and push the work onto a queue — otherwise the provider's retries will hammer you.

Route::post('/webhooks/app-payment', function (Request $request) {
    abort_unless(
        $request->header('X-Signature') === hash_hmac('sha256', $request->getContent(), config('app-payment.webhook_secret')),
        403
    );

    ProcessPaymentEvent::dispatch($request->all());

    return response()->noContent();
})->withoutMiddleware([VerifyCsrfToken::class]);

Store the provider's event ID and skip duplicates — webhook redelivery is normal.

Test without the network. Laravel's HTTP fake works as long as the package is built on Illuminate\Support\Facades\Http or accepts an injected Guzzle client. Verify that early; if it uses raw curl or a hardcoded new Client(), testing gets ugly and that's a strong signal to wrap it hard.

Http::fake([
    '*/payments' => Http::response(['id' => 'pay_123', 'status' => 'pending'], 201),
]);

$result = app(PaymentGateway::class)->charge($order);

Http::assertSent(fn ($request) => $request['reference'] === $order->uuid);

Multi-merchant setups. If you serve several merchants, the singleton binding breaks down. Bind a scoped instance per merchant using app()->scoped() or resolve a fresh client from a factory with explicit credentials.

Gotchas and Tips

Assume it is unmaintained. Zero stars, zero dependents, and no score means nobody is filing issues. Pin an exact version in composer.json and consider committing it to a vendor-patches/ path or forking it internally. Do not let a composer update silently change payment behaviour.

The README may be stale or missing. Derive the real API from src/. Look for @method docblocks, interfaces, and the concrete response class — response objects often expose toArray() and a magic __get, and which one you use determines how brittle your code is.

Money and currency. The single most common integration bug is units. Confirm whether the SDK expects minor units (cents) or a decimal string, and whether it converts for you. Write a test that asserts 2599 in becomes the value you expect on the wire by inspecting Http::assertSent.

Exception handling is probably coarse. If everything throws a single AppPaymentException, you cannot distinguish "card declined" from "API is down". Parse the HTTP status and the error code from the response body in your adapter and translate to your own domain exceptions. Retry only 5xx and connection errors — retrying a 4xx is how you generate duplicate disputes.

Timeouts default to something unhelpful. Set an explicit connect/read timeout and keep it below your queue worker's timeout. A hung payment call inside a queue job will stall the worker.

Config caching bites. If the provider reads env() directly rather than config(), php artisan config:cache will return null after deployment. Grep the source for env( and, if found, either avoid caching or patch the config file.

Debugging a failing call. Add global middleware rather than editing vendor code:

Http::globalRequestMiddleware(fn ($request) => $request->withHeader('X-Correlation-Id', (string) Str::uuid()));

Log the correlation ID with the order, or you'll be reconciling by timestamps later. For one-off inspection, Http::fake(fn ($request) => tap(null, fn () => dump($request->url(), $request->data()))) will show you exactly what the SDK is sending.

Webhook signature checks must use the raw body. $request->all() re-encoded is not byte-identical to what the provider signed. Use $request->getContent().

Extension points to look for. A Macroable trait on the client, a constructor that accepts a GuzzleHttp\ClientInterface, or an overridable baseUrl(). Any of these gives you a supported way to add endpoints or fake the transport. If none exist and the base URL is hardcoded in a class constant, that's the deciding factor: wrap it, don't extend it.

Version the API path explicitly. If the SDK hardcodes /v1/ in the URL, pin your expectations there — an API bump means the package must be patched, and nobody upstream is doing it.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings