serendipity_hq/bundle-stripe
A Symfony bundle for integrating Stripe payments, offering configuration and services to work with charges, customers, subscriptions, and webhooks via Stripe's PHP SDK. Helps wire Stripe into Symfony applications with less boilerplate.
This bundle is an archived Symfony wrapper around Stripe's PHP SDK from the pre-PaymentIntents era (Stripe API ~2017–2018). It is safe to read its source for historical patterns, but treat it as deprecated. Before committing, sanity-check whether a current stripe/stripe-php + a thin Symfony service is a better fit — for most greenfield apps it is.
Install and register
composer require serendipity_hq/bundle-stripe
For Symfony 4+/Flex era, register the bundle manually in config/bundles.php (it predates recipes so there is no auto-generated config):
SerendipityHQ\Bundle\StripeBundle\SerendipityHQStripeBundle::class => ['all' => true],
Configure
Create config/packages/serendipity_hq_stripe.yaml with your keys pulled from env vars (never commit secrets):
serendipity_hq_stripe:
api:
public_key: '%env(STRIPE_PUBLIC_KEY)%'
secret_key: '%env(STRIPE_SECRET_KEY)%'
First place to look
Because there is no maintained documentation, run:
php bin/console debug:config serendipity_hq_stripe
php bin/console debug:container stripe
Confirm the service ID that resolves to the Stripe API client (typically serendipity_hq.stripe.api or similar — the exact ID varies by version, which is itself a sign of how undocumented this package is). Autowiring by type is often unreliable here because the class name changed across Stripe SDK major versions, so wire by service ID.
First use case
The package was designed for the Charge API. In a controller:
use SerendipityHQ\Bundle\StripeBundle\Service\StripeApi;
use Stripe\Charge;
class CheckoutController
{
public function __construct(private StripeApi $stripe) {}
public function charge(int $amountCents, string $token): Charge
{
return Charge::create([
'amount' => $amountCents,
'currency' => 'eur',
'source' => $token,
'description' => 'Order #1234',
]);
}
}
Note that this calls the static Stripe\Charge::create — the bundle's job is mostly configuring Stripe::setApiKey() and exposing the client, so most of your code will still touch the underlying Stripe SDK directly.
Centralize key handling behind the bundle service. Resist setting \Stripe\Stripe::setApiKey() in controllers. Let the bundle do it once at container boot, and inject the API service everywhere else. If you find yourself adding a second Stripe::setApiKey call anywhere, the bundle isn't wired correctly.
Separate environments by config file, not by code. Maintain serendipity_hq_stripe.yaml with env-var placeholders, and set STRIPE_SECRET_KEY per environment (.env.test, CI secrets, etc.). Never branch on APP_ENV inside payment code.
Wrap charges in a domain service. Create an App\Payment\StripeGateway that depends on the bundle's API service and returns your own DTOs (ChargeResult, WebhookEvent), not raw Stripe objects. This is the single most important pattern for making an eventual migration away from this archived bundle cheap — swap the gateway implementation, leave callers untouched.
final class StripeGateway
{
public function __construct(private StripeApi $stripe, private LoggerInterface $logger) {}
public function charge(int $cents, string $token, array $meta = []): ChargeResult
{
try {
$charge = Charge::create([
'amount' => $cents, 'currency' => 'eur',
'source' => $token, 'metadata' => $meta,
]);
return ChargeResult::success($charge->id);
} catch (\Stripe\Exception\CardException $e) {
$this->logger->warning('Stripe card error', ['code' => $e->getStripeCode()]);
return ChargeResult::failure($e->getMessage());
}
}
}
Webhook controller. You'll typically build this yourself; the bundle's event dispatching (where present) is minimal and inconsistent across versions. Verify signatures with \Stripe\Webhook::constructEvent($payload, $sigHeader, $secret), dispatch to a Symfony Messenger bus or an internal event dispatcher, and return 200 quickly. Do the actual work asynchronously.
$event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
$this->bus->dispatch(new StripeWebhookReceived($event->id, $event->type, $event->data->object->toArray()));
return new Response('', 200);
Testing with the Symfony kernel. Because everything runs through static Stripe\* calls, unit tests can't mock the transport easily. Two workable approaches: (a) put your StripeGateway behind an interface and mock that; (b) use Stripe\ApiRequestor::setHttpClient(new \Stripe\HttpClient\CurlClient(...)) with a fake in integration tests. Option (a) is cleaner and worth the extra class.
Archived status is the top gotcha. The repo has been dormant since the Charge API era. It will not keep up with Stripe SDK major versions, PaymentIntents/SCA, StripeClient (the new object-based client), or the modern exception hierarchy (\Stripe\Exception\ApiErrorException vs. the old \Stripe\Error\*). Pin stripe/stripe-php in composer.json yourself and read its changelog before every bump — the bundle does not constrain it for you.
SCA / PSD2 compliance breakage. Charges with source will be rejected for European cards under 3DS regulation. If you need real EU payments, the Charge API path in this bundle won't cut it — you need PaymentIntents, which means bypassing the bundle's helpers and calling PaymentIntent::create yourself, or migrating off the bundle.
Amounts are in the smallest currency unit. A €10.00 charge is 1000, not 10.0. This trips up every new contributor; put the conversion in one place (a Money value object or the gateway) and never accept floats from controllers.
Idempotency is your responsibility. The bundle predates first-class idempotency ergonomics. Pass ['idempotency_key' => $orderUuid] as the second argument to ::create for anything retried (charges, refunds, payouts). Without it, network retries double-charge customers.
Webhook signature secret is separate from the API secret key. It's set in the Stripe dashboard per endpoint, not in your API keys. Store it as STRIPE_WEBHOOK_SECRET. Use the raw request body ($request->getContent()), not $request->request — parsing JSON before verification invalidates the signature.
Service IDs are unstable across versions. If autowiring fails, drop to php bin/console debug:container --show-private stripe or debug:container SerendipityHQ\\Bundle\\StripeBundle and grep the class list. Wire by id in services.yaml rather than type-hinting the API class, which has changed names repeatedly.
Config key drift between bundle releases. The YAML tree (serendipity_hq_stripe.api.secret_key vs. older serendipity_hq_stripe.secret_key shapes) changed across minor versions during its life. Always validate against debug:config serendipity_hq_stripe after upgrading.
Extension point worth knowing. If the bundle exposes an event subscriber hook for webhooks, prefer dispatching your own Symfony event from the controller instead — it keeps the integration testable and lets you swap to stripe/stripe-php directly later without touching subscribers.
Migration plan. Because the bundle is essentially a thin key-configuration shim, migrating is small: replace its bootstrap with new \Stripe\StripeClient($secret) in a Symfony service, inject that, and rewrite any static Charge::create calls to $client->charges->create. Keep your StripeGateway boundary intact and the rest of your app won't notice.
How can I help you explore Laravel packages today?