kj/payment-stripe-bundle
KJ Payment Stripe Bundle integrates Stripe payments into your PHP application, wrapping payment intents, checkout flows, and webhook handling behind simple configuration so you can accept cards and manage transactions with minimal boilerplate.
First, confirm what you are actually installing. kj/payment-stripe-bundle is a Symfony bundle (Symfony\Component\HttpKernel\Bundle\Bundle), not a Laravel package. It ships a DI extension and a config tree, not a ServiceProvider or a Facade. There is no bundle kernel in Laravel to register it with, so day-to-day "use" in a Laravel app means reading and lifting its wrapper code, not requiring it.
Inspect the constraints before touching composer.
composer show kj/payment-stripe-bundle --all
Check the require block. Expect a pinned, old stripe/stripe-php (~3/~4/~5) and possibly symfony/framework-bundle: ~2.8|~3.0. On PHP 8.1+ resolution will likely fail, or succeed by dragging an EOL Stripe SDK into your revenue path. Either outcome decides your approach.
Read the wiring, not the README. The entire API surface is three files:
DependencyInjection/Configuration.php — the config tree, keys, and defaultsDependencyInjection/*Extension.php — how parameters (e.g. %kj_payment_stripe.api_key%) bind into servicesResources/config/services.yml — the service ID you would otherwise injectWhatever that service wraps — a configured Stripe client, a charge helper — is the thing you are re-implementing.
First use case: set a secret key, create a Charge, read the failure. If the bundle (or extracted class) cannot cleanly do a single charge in isolation, it will not do refunds, webhooks, or SCA either.
Pragmatic path for a Laravel app: install the supported SDK and own the thin gateway yourself.
composer require stripe/stripe-php
// config/services.php
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
final class StripeGateway
{
public function __construct(private readonly StripeClient $client) {}
public function charge(int $amountCents, string $currency, string $token, array $meta = []): Charge
{
return $this->client->charges->create([
'amount' => $amountCents,
'currency' => $currency,
'source' => $token,
'metadata' => $meta,
], ['idempotency_key' => $meta['idempotency_key'] ?? Str::uuid()->toString()]);
}
}
The bundle's only value-add over this is ~50 lines of config plumbing you would write anyway.
Vendor, don't require. If you are porting behaviour out of an existing Symfony app, copy the service class into app/Services/ and treat it as vendored source. You inherit the naming and the call shapes without inheriting the abandoned dependency tree.
Keep payment logic behind an internal interface. The single durable architectural decision is reversibility:
interface PaymentGateway
{
public function charge(int $amountCents, string $currency, string $token, array $meta = []): Charge;
public function refund(string $chargeId, ?int $amountCents = null): Refund;
public function handleWebhook(Request $request): WebhookEvent;
}
Call sites depend on PaymentGateway, never on a Stripe class or a bundle service ID. When you later swap implementations, call sites never learn the bundle went away.
Strangler pattern for existing code. If legacy Symfony code already calls the bundle's service, put the interface in front of existing call sites first, then swap the implementation underneath. This is the only safe sequencing.
Always pass an idempotency key on charge and refund calls. Derive it from your own domain (order ID, attempt counter), not a random per-request UUID, or retries still duplicate.
Webhook handling is a route, not a controller action you forget about. Verify signatures with the raw request body — Laravel's parsed payload will not match. Reconcile webhook events against your own persisted charge records rather than trusting the event as the source of truth.
Config plumbing when porting the bundle's wiring. Replace %kj_payment_stripe.api_key%-style parameters with config('services.stripe.secret'), and replace the bundle service ID binding with a container binding:
$this->app->singleton(StripeClient::class, fn () => new StripeClient(config('services.stripe.secret')));
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Use the bundle as a reference for naming conventions only. Its charge/refund/callback method names are the one thing worth copying, because they probably match the domain language the rest of the KJ bundle family uses.
Bundle suffix is the tell. No ServiceProvider, no publishable config, no facade. Any tutorial implying otherwise is describing a different package. If a colleague asks you to composer require it into Laravel, stop them at the require block.parameters.yml, rotate anything that has been in version control.PaymentGateway interface, not the bundle's service definitions. Everything the bundle offers in the way of extensibility is Symfony container machinery that will not survive a framework migration.stripe/stripe-php or Cashier with your own wrapper over treating this as an upstream dependency.How can I help you explore Laravel packages today?