cartalyst/stripe
Cartalyst Stripe is a PHP/Laravel wrapper around the Stripe API, offering a fluent, object-oriented SDK for charges, customers, subscriptions, invoices, coupons, and webhooks. It streamlines payment integration with clean syntax and Laravel support.
Architecture fit. cartalyst/stripe is a thin, object-oriented adapter over Stripe's REST API, delivered as a Guzzle-backed PHP client with an optional Laravel service provider, Stripe facade, and publishable config/stripe.php. Architecturally it is plumbing: resource factories (customers(), charges(), paymentIntents(), subscriptions(), invoices(), refunds(), tokens(), plans(), coupons()) that map one-to-one onto Stripe endpoints and return plain PHP arrays. It resolves through the container singleton (stripe), which is the correct extension point for interception, logging, subclassing, or a gradual per-method migration. It is not a wrapper around stripe/stripe-php; it carries its own resource classes. That matters for any program that assumes "one Stripe client" — adopting this package alongside the official SDK means two client stacks, two response shapes (array vs \Stripe\* objects), and two exception hierarchies in the same codebase.
Critically for product planning: this package ships no billing logic. There is no proration engine, dunning state machine, trial lifecycle, tax determination, usage rating, or invoicing workflow. Subscriptions are CRUD over Stripe's objects. Everything above that line is internal scope, not vendor scope.
Integration feasibility. Low friction on the happy path. composer require cartalyst/stripe plus a vendor:publish for the service provider, then STRIPE_SECRET in .env. Multi-account support is the package's strongest genuine differentiator: a default profile plus a profiles array, with Stripe::profile('eu')->customers()->create([...]) at the call site. That unlocks regional entities, separate brands, and clean test/live separation without forking the integration — validate against your go-to-market structure before committing.
Technical risk. The release date is the risk, not the API surface. Last release 2021-10-09 (v2.4.6), 278 stars, zero dependents, no security-response commitment. Specific risks a TPM should own explicitly:
api_version in config rather than relying on the account default — otherwise a dashboard-side bump silently changes response shapes under you. Confirm the version the package targets is still accepted by Stripe.\Stripe\Webhook::constructEvent; you either pull stripe-php in just for HMAC verification or compute it yourself against the Stripe-Signature header (t and v1, signed payload ${t}.${body}, constant-time compare) before parsing JSON.charge vs. a payment_intent are different call shapes) — the exact spot where a version bump misbehaves silently.Key questions to resolve before greenlighting:
stripe/stripe-php anywhere? If so, plan for two client stacks or make the migration now.Stack fit. Laravel/PHP services are the natural home. The service provider auto-registers the Stripe facade and container binding; the published config/stripe.php supplies a default profile name and a profiles array for multi-account. The package is also framework-agnostic — instantiate Cartalyst\Stripe\Stripe directly in non-Laravel PHP — which helps if some services are outside Laravel. If billing is owned by a dedicated non-PHP service, this wrapper adds no leverage.
Migration path — build the exit on day one. The single most important architectural decision is not to call the facade from controllers or models. Put one internal interface in front of it immediately, shaped around our operations rather than Stripe's resource tree:
interface BillingGateway
{
public function createCustomer(User $user): array;
public function charge(int $amountCents, string $currency, string $paymentMethodId, string $customerId): array;
public function refund(string $paymentIntentId, ?int $amountCents = null): array;
}
Implement it with Stripe::… today; swap to stripe/stripe-php or Cashier later without touching call sites. This is both good design and migration insurance, and it converts the dependency into a contained, replaceable adapter — a bounded backlog item with a named owner, not an emergency and not a re-architecture. The container stripe binding is the cleanest interception point for per-request logging and for a gradual, per-method cutover to the official SDK.
Compatibility constraints to lock down:
composer require in a throwaway branch, not by reading the README.api_version pinned explicitly in config; never inherit the account default.Sequencing (recommended order of work):
api_version is accepted by Stripe. Gate: all three pass, or adoption becomes a rewrite with no intermediate state.BillingGateway. No direct facade calls anywhere else, ever.refunds()->create call site to confirm which identifier it passes.Data modeling guidance to enforce from the start: store IDs (cus_…, pi_…, sub_…, in_…) with a stripe_account column for multi-profile installs; treat the database as an index into Stripe, not a copy of it. Pass a deterministic idempotency key on every mutating call ("refund:{$order->id}", not a random UUID) — it is the only protection against double charges from retried jobs, double-clicked submits, and queue redeliveries. Centralise minor-unit formatting; do not scatter * 100 through the codebase (JPY and KRW take 1999 meaning ¥1999).
Maintenance. Effectively unmaintained upstream. Zero dependents and a release three-plus years old is not a support story — internal engineering owns the code, the security posture, and the Stripe-compatibility posture. The mitigation is structural: the code is a thin adapter over a well-documented public API, the BillingGateway interface bounds the blast radius, and the migration is schedulable rather than urgent. Name an owner and put the migration on the roadmap with a horizon, rather than leaving it as ambient risk.
Support. No vendor support, no security-response commitment, no SLA. Community-only for questions. Escalation path is internal engineering plus reading src/Api/*.php when behavior is undocumented — each resource is a thin wrapper over POST /v1/..., so the source is the specification. Budget for that onboarding cost; there is no vendor to call.
Scaling. Synchronous Guzzle HTTP against Stripe's rate limits, so throughput is bounded by Stripe rather than by the package. Push all mutating calls onto queue workers with retries and deterministic idempotency keys; keep webhook processing on its own queue so Stripe retries do not back up customer-facing traffic. Multi-profile configuration scales cleanly across accounts; per-account rate budgets and per-account keys need to be tracked deliberately rather than assumed.
Failure modes to instrument for:
2.4.6 boundary).* 100.\Stripe\* exceptions instead of Cartalyst\Stripe\Exception\StripeException subclasses; register a handler, log error type and decline code, return a generic message to the customer, never the raw message.Authorization header.Ramp-up. One to two days for a competent Laravel developer to become productive: config/stripe.php first, then the facade root and src/Api/ class names (self-documenting and one-to-one with REST endpoints), then the array-vs-object difference from the official SDK — that is the biggest day-to-day adjustment and the most common source of bugs for anyone with prior stripe-php experience. Budget separately for the billing logic the package does not ship: proration, dunning, trial state machines, tax, and revenue recognition are product scope discovered in week three if not planned in week one. Treat the contract-test suite as the onboarding artifact that makes the boundary legible to new hires, and document the BillingGateway interface as the sanctioned entry point so no one shortcuts to the facade.
How can I help you explore Laravel packages today?