herdwatch/stripe-bundle
HerdWatch Stripe Bundle integrates Stripe payments into Symfony applications, with configurable API credentials, payment intent and checkout support, webhook handling, and reusable services for common billing workflows. It helps teams add secure card payments and subscription-ready Stripe flows with...
First, the framework mismatch. herdwatch/stripe-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle subclass, a DependencyInjection/*Extension, and Doctrine mappings β none of which Laravel's container reads. You cannot composer require it into a Laravel app and get working services. The v3.1.4 release changes nothing about that: the patch is a Doctrine DBAL type-mapping fix, i.e. work inside the Symfony/Doctrine integration. If you were hoping a later release added a Laravel provider, it didn't.
Realistically you have three options:
stripe/stripe-php directly (or Cashier if you want subscription billing abstractions).If you are on Symfony, the minimal path:
composer require herdwatch/stripe-bundle:3.1.4
Then register it manually in config/bundles.php β with no Flex recipe, auto-registration will not happen:
return [
// ...
Herdwatch\StripeBundle\HerdwatchStripeBundle::class => ['all' => true],
];
Set credentials in .env (STRIPE_SECRET, STRIPE_PUBLISHABLE, and a webhook signing secret) and add config/packages/herdwatch_stripe.yaml if the bundle's extension requires explicit config β check whether it has a Configuration class with a mandatory root node, otherwise container compilation fails with an unhelpful "unrecognized option" error.
Where to look first, in order:
README.md β for the actual service IDs and config tree. With a package this small, the README may be thin; do not assume.src/DependencyInjection/*Extension.php and Configuration.php β this is the authoritative list of config keys and service aliases. Everything else is guesswork until you read these.src/Resources/config/ β service definitions reveal whether the Stripe client is public, aliased, or autowired by interface.Entity/, Model/, or Resources/config/doctrine/ directory β this is where the "save Stripe objects in database" feature lives, and it determines your migration burden. This is also the code that v3.1.4 touched (see Gotchas on decimal types).First use case: inject the bundle's Stripe client service into one console command and make a single read call ($client->customers->retrieve(...) or the bundle's wrapper equivalent). That proves credentials, DI wiring, and API version compatibility in one step before you touch persistence.
Inject the client; never call Stripe\Stripe::setApiKey() globally. The whole point of wrapping the SDK in a bundle is a configured, shared client. Global static key setting leaks across tests and long-running workers, and it will fight whatever the bundle's factory does.
Webhook-first design. Treat the bundle's webhook handling (if present) as the entry point for all state changes, and keep your controllers thin:
$request->getContent() before any listener touches it.id in a unique-constrained table before processing. That gives you idempotency for free and a replay log when something breaks.Persisting Stripe objects. The bundle's Doctrine integration is the risky, high-value part. Pattern that holds up:
string(255) column with a unique index β never as the primary key, and never as an integer.stripe_id, your own autoincrement/UUID PK, and a payload JSON column holding the raw object. Stripe's schema drifts with API versions; the raw JSON is your escape hatch when a field you didn't model becomes important.created_at/updated_at from Stripe's timestamps plus your own synced_at, so you can find stale rows.string to match DBAL's own decimalβstring behaviour. Follow the same convention in your own entities ($table->decimal(...) in Eloquent terms, cast to string or a money value object) so the bundle's mapped entities and yours don't disagree about types at hydration time.Sync vs. webhook. For backfills and reconciliation, write a command that pages through ->all(['limit' => 100, 'starting_after' => $lastId]) and upserts. Run it on a schedule to catch webhooks you missed during downtime. Upsert on the Stripe ID, and make it idempotent so re-running is safe.
Testing. Mock at the boundary, not at the bundle. If the bundle exposes an interface for the client, bind a fake in the test container; otherwise stub HTTP with a recorded fixture set. Never hit the live API in CI β and be aware the SDK will happily attempt real network calls if a key is present. If you assert on hydrated entities, assert percentages/decimals as strings, since that is now the mapped contract.
Pin an exact version ("herdwatch/stripe-bundle": "3.1.4" not "^3.1"). The 3.1.4 release is a single-PR patch (FIT-8548, first contribution from @kbaryshev-hw) that changes how decimal percent fields are mapped β a type contract change, not a bugfix you can shrug at. Anything that serialises, compares, or persists those fields can shift from float to string. It's non-breaking at the API level but it is behaviourally visible, so vendor the source into a review branch and read the mapping change before deploying. Treat the CHANGELOG as your only change signal; there is no dependents graph to tell you what breaks.
API version drift is the most likely failure mode. The bundle pins a stripe/stripe-php constraint; your app needs a version too. Composer will resolve to the intersection, which may be an old SDK whose pinned API version no longer matches your Stripe account's default. Symptoms are InvalidRequestException on fields that "obviously" exist. Check the resolved SDK version with composer show stripe/stripe-php and compare against the account's API version in the dashboard. If the bundle sets a hard-coded apiVersion, you may need to override the client service.
Decimal and percent fields are strings. DBAL returns DECIMAL as string in PHP; v3.1.4 aligned the bundle's percent fields with that instead of fighting it. Consequences: $coupon->percent_off === 25.0 is false, (float) casts silently lose precision on long decimals, and json_encode of a hydrated entity emits "25.00" where a front-end expects a number. Cast at the edge of your app (view models / API resources), not inside the entity, and write tests that pin the string form.
Doctrine mapping surprises. Bundles that ship mappings often assume auto_mapping is on and a specific bundle alias. If your doctrine.orm config uses explicit mappings: blocks, the bundle's entities silently won't register and you get "class does not exist" at runtime, not compile time. Also: bundled migrations do not exist β you generate the schema diff yourself, and you'll want to review it rather than accept whatever doctrine:migrations:diff produces, especially around index names and any column whose type just changed from float to string (that diff will be destructive; treat it as a hand-written migration).
Extension points, in order of preference:
config/services.yaml by redefining the same service ID after the bundle loads.decorate: the client or repository service with your own wrapper (retry logic, logging, circuit breaking) β cleaner than subclassing.new.Debugging toolkit. bin/console debug:config herdwatch_stripe shows the merged config tree; debug:container --tag=... and debug:container 'herdwatch' find the actual service IDs when the README is wrong; the profiler's Doctrine panel confirms whether the bundle's entities are actually mapped (look for the table names). When a value comes back in the "wrong" type after upgrading, debug:container --parameter plus the Doctrine panel will tell you whether the bundle's custom type or the plain DBAL type handled the column.
Webhook secrets and multiple environments. If the bundle has a single webhook_secret config key, you cannot serve multiple Stripe endpoints (test vs. live, or multiple connected accounts) from one app instance. Verify this early if you're doing anything multi-tenant β it's the kind of limitation that forces a fork. A patch release that only touches type mapping is a reminder that feature-level gaps here are unlikely to close on their own.
Laravel translation note. If you're reading this as a Laravel dev porting the ideas: the Doctrine "save Stripe objects" pattern maps to Eloquent + $table->string('stripe_id')->unique() + a json column, and the bundle's DI wiring maps to a singleton in AppServiceProvider or a dedicated StripeClient binding. The webhook idempotency table, the raw-payload JSON column, and the reconciliation command are the three pieces actually worth copying β and add a fourth from v3.1.4: decide up front that decimal/percent columns are strings everywhere, in the database, in the model, and in the API resource.
How can I help you explore Laravel packages today?