flosch/stripe-bundle
Symfony bundle integrating the Stripe PHP library, with service configuration and dependency injection for convenient access to Stripe payments, customers, subscriptions, and webhook handling.
The framework-mismatch reality check comes first. flosch/stripe-bundle is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no facade, no config/stripe.php to publish, and no php artisan vendor:publish. The FloschStripeBundle class only registers with Symfony's kernel — so composer require flosch/stripe-bundle inside a Laravel app gets you a transitive pull of stripe/stripe-php and nothing else. Your day-to-day leverage has two shapes: (a) if you also maintain a Symfony app, use the bundle as intended, or (b) if you're Laravel-only, read the bundle's DI wiring as a reference and copy the pattern into a service provider — do not try to install it.
If you actually have a Symfony codebase (Symfony 3–5 era):
composer require flosch/stripe-bundle
// config/bundles.php
Flosch\Bundle\StripeBundle\FloschStripeBundle::class => ['all' => true],
# config/packages/flosch_stripe.yaml
flosch_stripe:
api_key: '%env(STRIPE_API_KEY)%'
api_version: '2024-06-20' # pin it — see Gotchas
publishable_key: '%env(STRIPE_PUBLISHABLE_KEY)%'
Where to look first (the README is thin and lags the code):
vendor/flosch/stripe-bundle/DependencyInjection/Configuration.php — the real config tree; this is the source of truth for what you can set.vendor/flosch/stripe-bundle/Resources/config/services.xml — the actual service id (typically flosch_stripe.client, aliased to Stripe\StripeClient). Confirm both before injecting.First use case — inject the client by type and create a PaymentIntent:
public function __construct(private \Stripe\StripeClient $stripe) {}
public function createIntent(int $amount, string $currency, string $customerId): \Stripe\PaymentIntent
{
return $this->stripe->paymentIntents->create([
'amount' => $amount,
'currency' => $currency,
'customer' => $customerId,
'automatic_payment_methods' => ['enabled' => true],
]);
}
The Laravel equivalent (what you actually build) is about 30 lines: a config/stripe.php, a singleton StripeClient binding in AppServiceProvider, and a Stripe facade if ergonomics warrant it. Write that instead of fighting Composer.
Laravel-first pattern: first-party wrapper, not the bundle. The bundle's entire job is: read three config values, call Stripe::setApiKey(), expose a configured client. Reproduce that:
// config/services.php
'stripe' => [
'key' => env('STRIPE_SECRET'),
'publishable' => env('STRIPE_PUBLISHABLE'),
'api_version' => env('STRIPE_API_VERSION', '2024-06-20'),
],
// AppServiceProvider::register()
$this->app->singleton(\Stripe\StripeClient::class, function ($app) {
return new \Stripe\StripeClient([
'api_key' => config('services.stripe.key'),
'api_version' => config('services.stripe.api_version'),
]);
});
This gives you the same DI ergonomics as the bundle with full control over API version pinning and StripeClient options — neither of which the bundle exposes cleanly.
Symfony side: inject by interface, not alias. If you're on the bundle, type-hint Stripe\StripeClient (or the service id flosch_stripe.client) rather than autowiring by config. That way, if you later replace the bundle with a hand-written service definition, call sites don't change. This is the whole point of "quiet exit" plan — keep it low-friction.
Webhook handling is on you, either way. The bundle provides zero webhook support — no controller, no signature verification, no idempotency helper. Your pattern, independent of framework:
public function handle(Request $request)
{
$payload = $request->getContent();
$sig = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sig, config('services.stripe.webhook_secret')
);
} catch (\UnexpectedValueException | \Stripe\Exception\SignatureVerificationException $e) {
return response()->noContent(400);
}
// persist event.id BEFORE processing — dedupe on replays
// then dispatch to a queued job, not inline
return response()->noContent(200);
}
Config caching / env-driven keys. Laravel caches config on deploy with artisan config:cache; the bundle's YAML path effectively does the same on Symfony. Either way, keys must come from env, never from committed YAML or config/services.php defaults. Never log the resolved client — Symfony's debug:container and Laravel's php artisan tinker will both happily print the client object with the key inside.
Idempotency and retries are not provided. Wrap mutating Stripe calls with an idempotency key derived from your own domain event (e.g. order_paid_{order_id}) whether you use the bundle or a hand-rolled provider. The SDK supports 'idempotency_key' => ... on the request options; the bundle neither encourages nor blocks this, so add it explicitly.
Test mode discipline. Keep sk_test_* vs sk_live_* in separate env vars per environment and assert at boot time (feature test or a config:cache-time check) that the key prefix matches APP_ENV. This is a one-liner either way but worth codifying because nothing in the bundle guards it.
Composer will fight you on modern PHP/Symfony. The bundle targets Symfony >= 3, which pins framework-bundle versions that are EOL. Composer resolution failures — especially during an unrelated dependency bump — are the expected failure mode, not the exception. Do the composer require flosch/stripe-bundle on a throwaway branch of the real app before committing to any plan; the exact conflict message settles build-vs-buy in one hour.
API version drift is the highest-impact silent failure. The Stripe SDK pins a default API version, and the bundle doesn't loudly expose the pin. If your Stripe dashboard's version differs, response and webhook event shapes can change without erroring. Always pass api_version explicitly at client construction and record the value in a deploy note.
Webhook signature verification is not implemented. If you adopt a pattern from anywhere that "works" without Webhook::constructEvent and STRIPE_WEBHOOK_SECRET, that's the highest-severity bug in any Stripe integration. Forged events look just like real ones. There is no shortcut here and the bundle does not help.
The publishable_key config is a trap for expectations. It's exposed mainly for convenience; it does not configure Stripe.js, Elements, or Checkout behavior. Frontend wiring is your problem.
Single-maintainer, zero-dependents supply-chain posture. 30 stars, 0 dependents, no visible CI or security policy, revenue-adjacent domain. If procurement or security review is involved, expect pushback and prepare the answer: "we're using the SDK directly, this is only a reference." For a from-scratch Laravel app, that's the honest default.
Debug output leaks keys. debug:container, dump($stripe), and log exceptions on Stripe API errors can all print a live key. Scrub in your Log tap / Symfony MonologBundle config and never dd() the client. The bundle adds no redaction.
Extension points, if you must extend the Symfony bundle: override the service definition in your own services.yaml after the bundle loads (Symfony's later-loaded config wins), or decorate the client. Do not edit vendor/ — you'll lose the change on composer update and you'll be asked about it in code review. If you need anything beyond key injection, write your own service; the bundle's surface is too thin to extend productively.
Migrating off the bundle (existing Symfony app): define the client explicitly in config/services.yaml, delete the bundle from bundles.php and composer.json, keep the call sites using Stripe\StripeClient unchanged. This is the low-risk exit; do it well before a Symfony major upgrade, because the EOL constraint will block you eventually.
When the answer is "just don't." Laravel stack, Symfony 6/7, subscription lifecycle needs, or any PCI/audit scope — the bundle adds nothing. stripe/stripe-php + Cashier (Laravel) or a 10-line service definition (Symfony 6/7) is smaller, maintained, and on your own update cadence. Forking the bundle to keep it alive is strictly worse than owning a first-party wrapper, because the wrapper only encodes your decisions.
How can I help you explore Laravel packages today?