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

Stripe Bundle Laravel Package

miracode/stripe-bundle

A Laravel package that bundles Stripe integration essentials, helping you add payments, webhooks, and billing to your app with less boilerplate. A convenient starting point for Stripe in Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no service provider, no config/stripe.php, and no Cashier-style Eloquent models. The entire value proposition is "register stripe/stripe-php in the Symfony DI container and read your keys from config." If you are in a Laravel codebase, the correct day-one move is composer require stripe/stripe-php directly and bind your own singleton — but reading this bundle is still worthwhile as a reference implementation, and the patterns below map cleanly.

Minimal path (Symfony service)

  1. Check constraints before require. Open the bundle's composer.json on GitHub first. Confirm:

    • declared symfony/* range (a ^4.4|^5.0 constraint will fail on Symfony 6/7 apps),
    • php range,
    • whether it depends on stripe/stripe-php (wraps the SDK) or ships its own HTTP client (rots fast against Stripe's dated API versions).
  2. composer require miracode/stripe-bundle
    
  3. Register the bundle. Flex usually does this automatically into config/bundles.php:

    return [
        // ...
        Miracode\StripeBundle\MiracodeStripeBundle::class => ['all' => true],
    ];
    

    Older/non-Flex setups need this added manually — if the bundle "does nothing," check here first.

  4. Read the README for the exact config root key (miracode_stripe, stripe, miracode_stripe_bundle, …) and the service ID. Do not guess either. One wrong root key yields a silent no-op or a container compile error with a confusing message.

  5. Keys into .env.local, never .env:

    STRIPE_PUBLIC_KEY=pk_test_...
    STRIPE_SECRET_KEY=sk_test_...
    STRIPE_WEBHOOK_SECRET=whsec_...
    
  6. bin/console cache:clear
    

    Symfony caches the compiled container aggressively. After any bundle config change, a stale container will keep serving the old values and you will burn an hour debugging a config that is already correct.

Where to look first

  • DependencyInjection/ — the extension class and Configuration.php. This is the whole bundle. Ten minutes of reading tells you every config key and every service it registers.

  • Service discovery:

    bin/console debug:config miracode_stripe     # resolved config values
    bin/console debug:container --show-private | grep -i stripe
    bin/console debug:autowiring stripe
    

    debug:autowiring is the fastest way to find the alias you can type-hint.

First use case

Inject the client into a controller and create a Checkout Session — the smallest end-to-end loop that proves keys, container wiring, and network egress all work:

#[Route('/checkout/{order}', name: 'checkout', methods: ['POST'])]
public function checkout(Order $order, \Stripe\StripeClient $stripe): Response
{
    $session = $stripe->checkout->sessions->create([
        'mode' => 'payment',
        'line_items' => [[
            'price_data' => [
                'currency' => 'usd',
                'unit_amount' => $order->getTotalInCents(),
                'product_data' => ['name' => $order->getDescription()],
            ],
            'quantity' => 1,
        ]],
        'success_url' => $this->generateUrl('checkout_success', [], UrlGeneratorInterface::ABSOLUTE_URL)
            . '?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => $this->generateUrl('cart', [], UrlGeneratorInterface::ABSOLUTE_URL),
        'client_reference_id' => (string) $order->getId(),
    ], ['idempotency_key' => 'order-'.$order->getId()]);

    return $this->redirect($session->url);
}

If debug:autowiring doesn't show Stripe\StripeClient, the bundle exposes a different concrete class or a wrapper — use whatever it registers. Do not instantiate new StripeClient(...) yourself; that defeats the point of the bundle and splits key management in two places.


Implementation Patterns

Inject the client, don't fetch from the container

Constructor injection into a domain service is the pattern the bundle is built for:

final class StripePaymentGateway
{
    public function __construct(private readonly \Stripe\StripeClient $stripe) {}
}

Avoid $container->get('stripe.client') in application code. It hides the dependency, breaks static analysis, and makes the eventual swap to a first-party adapter a grep-and-pray exercise.

Wrap it behind your own interface — always

This is the single highest-leverage pattern for a 9-star, 0-dependent dependency. The bundle should never appear in a controller signature after week one:

interface PaymentGateway
{
    public function charge(Order $order): PaymentResult;
    public function refund(string $paymentIntentId, ?int $amount = null): void;
}
# config/services.yaml
App\Payment\PaymentGateway:
    alias: App\Payment\StripePaymentGateway

Now the bundle is an implementation detail. When you fork it, pin it, or replace it with Cashier/stripe-php, exactly one class changes.

Mode and key handling

Most thin wrappers expose a mode / sandbox / test flag and derive the API base or accept two key pairs. Two workable patterns:

  • One config key per environment, values resolved from env vars and .env.local / Symfony secrets — simplest, least surprising.
  • A sandbox boolean plus paired keys — check the README, because if the bundle builds the client from a single secret key you do not want to fight it.

Assert at boot that the key prefix matches the environment:

// kernel boot or a compiler pass
if ($this->environment === 'prod' && !str_starts_with($_ENV['STRIPE_SECRET_KEY'], 'sk_live_')) {
    throw new \RuntimeException('Non-live Stripe key in production.');
}

This is the cheapest possible guard against the most expensive possible mistake.

Webhook endpoint — own it, don't expect the bundle to

The description claims "extensible foundation for webhooks"; treat that as "it does not do them for you." Build the endpoint yourself:

#[Route('/stripe/webhook', methods: ['POST'])]
public function webhook(Request $request, WebhookService $webhooks): Response
{
    $payload = $request->getContent();
    $sig = $request->headers->get('Stripe-Signature', '');

    try {
        $event = \Stripe\Webhook::constructEvent($payload, $sig, $_ENV['STRIPE_WEBHOOK_SECRET']);
    } catch (\Stripe\Exception\SignatureVerificationException) {
        return new Response('', 400);
    }

    $webhooks->handle($event);   // dedupe on $event->id, then dispatch to a message bus
    return new Response('', 200);
}

Non-negotiables inside handle():

  • Idempotency store keyed on $event->id — a UNIQUE index on a processed_stripe_events table. Stripe retries; without this you double-provision or double-ship.
  • Queue the work, acknowledge fast. Return 200 immediately after enqueueing; do the provisioning in a Messenger handler. Long webhook handlers time out and trigger retries, which is how you get the duplicate you just guarded against.
  • Verify against the raw body, before any middleware mutates it.

Console commands for reconciliation

Because the bundle only gives you the client, any "the webhook never arrived" recovery is yours. A scheduled command that lists recent PaymentIntents and reconciles against local orders covers you for dropped webhooks and for replaying a bad deploy:

$intents = $stripe->paymentIntents->all([
    'created' => ['gte' => $since->getTimestamp()],
    'limit' => 100,
]);

Testing

Point the bundle's config at test keys in config/packages/test/. For unit tests, mock the interface (PaymentGateway), never the StripeClient — the SDK's method chains are built from magic __call proxies and are miserable to mock. For integration tests against real Stripe, use sk_test_ plus Stripe test clocks and test card numbers.

Laravel-side translation: everything above is AppServiceProvider::register() + a config/stripe.php + the same PaymentGateway interface. Cashier replaces the interface entirely if your billing model is conventional subscriptions.


Gotchas and Tips

  • It is not a Laravel package. No StripeServiceProvider, no facade, no publishable config. Adding it to a Laravel app means pulling in a bundle whose only value — Symfony DI wiring — registers to a container Laravel does not have. Do not try to bridge this; write a 30-line service provider around stripe/stripe-php.

  • Symfony constraint collision is the most likely failure at install time. Laravel 11/12 pin symfony/* 6/7. A bundle declaring ^4.4|^5.0 hard-fails composer update or triggers a partial-update cascade. Check composer.json on GitHub before requiring, not after.

  • Stale container after config edits. bin/console cache:clear after every bundle config change, especially in dev. A cached container will happily serve old keys and send you hunting ghosts.

  • Wrong config root key = silent no-op. If keys aren't loading, run bin/console debug:config <root> and confirm the values are actually merged. If the tree name is wrong, Symfony ignores your YAML without complaint.

  • Stripe API version drift. The SDK is pinned; Stripe ships dated API versions with deprecation windows. Budget a recurring calendar reminder to bump stripe/stripe-php and read the changelog. This bundle gives no evidence anyone is tracking that for you.

  • Money is in minor units, always integers. unit_amount: 10.00 is invalid or is ten cents depending on the endpoint. Keep a single Money value object with getAmountInCents() and never do float arithmetic on prices.

  • Always pass an idempotency key on POSTs — ['idempotency_key' => "..."] as the second argument. Retries, double-clicks, and webhook-driven re-attempts are otherwise duplicate charges.

  • Do not build card forms that post to your server. Use Stripe Checkout or Elements. If 4242… or a PAN ever touches your application, you've jumped from SAQ A into a real PCI scope. That's a compliance boundary, not a style preference.

  • Extension points are the container itself. There are no events or plugin hooks you should count on. Override by decoration:

    App\Payment\InstrumentedStripeClient:
        decorates: <the bundle's service id>
        arguments: ['@.inner', '@logger']
    

    That's also how you'd add logging, metrics, or a timeout wrapper around outbound calls — which the bundle does not provide, and which you need.

  • Vendor and pin. 9 stars, 0 dependents, single maintainer. Copy the source into your repo (or a private fork), pin the SDK, name an internal owner, and set a review date on the risk register. An unowned fork of an unowned package compounds.

  • Instrument the failure modes you will actually hit: Composer resolution failure in CI (not prod), webhook 4xx rate after a secret rotation, duplicate-event processing without a dedupe table, and non-live keys in prod. Every one of these is silent by default.

  • Opportunity score ≠ undervalued asset. A high "opportunity" sub-score with a low composite means "unclaimed niche," which usually means nobody needed it badly enough to maintain it. Read it as a reference implementation, adopt it as a dependency only for low-revenue Symfony surfaces with a hard timebox.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect