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

Checkout Laravel Package

milzer/checkout

Milzer Checkout is a Laravel package for adding flexible checkout flows to your application. It helps manage carts, create orders, and integrate payment steps through a clean, extensible API designed for modern PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Treat this as an unvetted package: with zero stars, zero dependents, and a single maintainer, you are the QA team. Budget an hour to read the source before you write a line of application code.

Install and publish

composer require milzer/checkout
php artisan vendor:publish --provider="Milzer\Checkout\CheckoutServiceProvider"
php artisan migrate

Publish tags individually (--tag=checkout-config, --tag=checkout-migrations) so you can review each artifact before it lands in your repo.

Read these four things, in this order

  1. src/CheckoutServiceProvider.php — tells you what is bound as a singleton, what is bound to an interface, and which events/listeners are registered. This file is the package's table of contents.
  2. config/checkout.php — gateway driver, currency, tax strategy, route prefix, middleware. Anything here that you don't set explicitly is a decision the package made for you.
  3. The published migrations — the schema is the real API. Look for the order/line-item/payment/transaction tables, nullable columns, and any status enum. Those enum values are the states your app must handle.
  4. src/Contracts/ (or src/Contracts/*.php) — the interfaces you should type-hint. If the package ships contracts, never resolve concretes directly.

Your first use case

Build the smallest end-to-end path before touching discounts or tax: create a cart, add one line item, start a checkout session, redirect to the gateway's hosted page, receive the return/webhook, and confirm the order flips to a paid state. Write down the state transition you observe. Everything else in this package is an elaboration on that loop, and if the loop is broken for your use case, you want to know on day one, not after you've wired up five listeners.

If the package has a test suite, run vendor/bin/phpunit (or Pest) inside vendor/milzer/checkout after a fresh install. That is the cheapest possible smoke test of whether the package works with your Laravel and PHP versions.

Implementation Patterns

Resolve through the container, not new

If the package exposes contracts, bind your own implementation in a service provider and type-hint the interface everywhere — controllers, jobs, listeners. This is your only real escape hatch if the default implementation turns out to be wrong for you, and it costs nothing to do from the start.

public function register(): void
{
    $this->app->bind(
        \Milzer\Checkout\Contracts\CheckoutGateway::class,
        \App\Checkout\StripeGateway::class
    );
}

Recompute totals server-side, always

Client-submitted line items should carry a product/SKU identifier and a quantity, nothing more. Prices, tax, and shipping are resolved from your catalogue inside the checkout flow. If the package's API lets you pass a price through, treat that as a test-only convenience and never trust it from a web request.

Wrap the initiation in a transaction with an idempotency key

$order = DB::transaction(function () use ($cart, $request) {
    $order = $this->checkout->createOrder($cart);      // verify actual method names in source
    $order->update(['idempotency_key' => $request->header('Idempotency-Key')]);
    return $order;
});

Double-submits on a "Pay now" button are the single most common source of duplicate charges. Generate the key client-side per checkout attempt and store it with a unique index.

Let events do the fan-out

Check what the package dispatches after payment confirmation. A typical set is CheckoutStarted, PaymentSucceeded, PaymentFailed, OrderCompleted. Register listeners for the terminal events rather than calling your own services inline from the webhook controller:

  • Fulfilment/dispatch
  • Receipt email
  • Inventory decrement
  • Analytics/CRM sync

Keep listeners ShouldQueue and implement ShouldBeUnique where double-processing would be harmful. If the package fires events inside a database transaction, add $afterCommit = true to the listener or wrap the dispatch — otherwise a queued worker can read a row that hasn't committed yet.

Webhooks: verify, persist, 200, then queue

public function __invoke(Request $request, CheckoutWebhookHandler $handler)
{
    abort_unless($handler->verify($request), 403);   // signature check first
    $event = CheckoutWebhook::firstOrCreate(
        ['gateway_event_id' => $request->input('id')],
        ['payload' => $request->all()]
    );
    ProcessCheckoutWebhook::dispatch($event)->onQueue('webhooks');
    return response()->noContent();  // fast ack; do work off-thread
}

Gateways retry on timeout. The firstOrCreate on the event ID is your idempotency guard, not the payment status check.

Testing pattern

Use RefreshDatabase, a fake gateway bound in the test container, and Event::fake() to assert the right domain events fire without touching a real payment provider. The highest-value test you can write is "webhook delivered twice results in one order fulfilment."

Gotchas and Tips

The package is a black box until you read it. With zero dependents there is no Stack Overflow corpus, no blog posts, no issue history to learn from. Anything you don't understand in src/ is a support burden you own. Pin the exact version in composer.json ("milzer/checkout": "1.2.3"), not a caret range, and re-read the diff on every bump.

Money is probably integers. Check whether amounts are minor units (cents) or decimal. Mixing the two produces orders that are off by 100x, and the bug surfaces only at the gateway. Confirm by reading the migration column types — unsignedBigInteger means cents, decimal(10,2) means units.

config:clear after publishing. If you publish the config while config:cache is active, your edits appear to do nothing. This burns everyone once.

Exclude the webhook route from CSRF. It's a machine-to-machine POST with no token. Add it to $except in bootstrap/app.php (Laravel 11+) or VerifyCsrfToken — and confirm the package doesn't already do this from its service provider, since a route registered with the web middleware group will be blocked by default.

Published migrations are a one-way door. The package's own tests run against its internal migrations, not yours. If you edit the published files (add an index, widen a column), a future package update can silently diverge from your schema. Prefer additive follow-up migrations and leave published files byte-identical.

Watch for environment leakage. Gateway keys belong in config/services.php or .env, never in a committed config file. If the published config/checkout.php contains a default pk_test_... value, replace it with env('...') before it reaches version control.

Events inside transactions, revisited. Even without the afterCommit subtlety, a payment event listener that throws will roll back the order row if it runs synchronously inside the transaction — leaving a charged customer with no record. Queue the listeners.

Extension points to look for

  • Interfaces in src/Contracts/ — bind your own.
  • A Checkout::extend() or driver registration method on the manager, if the package follows Laravel's Manager pattern.
  • Config-driven driver selection, which lets you swap sandbox and live without code changes.
  • Events — the least invasive hook and the one most likely to survive a version bump.

Debugging slow or failed payments

Log the outgoing gateway request and the raw response at debug level with the auth header redacted, and correlate via the idempotency key and gateway event ID. If Laravel Telescope is available, the outgoing HTTP client entries give you this for free. When an order is stuck, check three things in order: was the webhook received (your checkout_webhooks table), did the job run (failed_jobs), and does the gateway's own dashboard agree with your order status. Divergence between the gateway and your DB almost always means a dropped webhook, not a code bug.

Have a reconciliation command. Because this package has no community track record, assume a webhook will eventually be missed. A scheduled php artisan checkout:reconcile --since=24h that pulls recent gateway transactions and replays the state transitions is worth writing before launch, not after the first incident.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource