simpleweb/saas-bundle
SimpleWeb SaaS Bundle is a PHP package that bundles common SaaS building blocks such as plans, subscriptions, billing, and account management, helping Laravel/PHP developers add subscription features faster and focus on their core product.
Read this first: the package is a Symfony 2 bundle, not a Laravel package. There is no composer require simpleweb/saas-bundle path into a Laravel app — its Symfony/Doctrine constraints are PHP 5.x/7.0-era and will conflict on PHP 8+/Laravel 10+. On a Laravel codebase, your day-to-day use is as a reference model to port, not a dependency to install. Everything below assumes that framing unless you are explicitly on a legacy Symfony 2 host.
Minimal steps to get value in a day:
Entity/ (the domain model), DependencyInjection/Configuration.php (what's actually configurable — the README is likely stale), Event/ + EventListener/ (hook points), Gateway/ (how provider calls are shaped).Plan records, create a Subscription for a user/team against one, and gate a single feature on that subscription's status. That one path exercises models, plan lookup, state, and gating — and tells you in hours whether the bundle's design is worth more of your time.If you are on a Symfony 2 host: register Simpleweb\SaaSBundle\SimplewebSaaSBundle in AppKernel, import the bundle extension config, override the entity class parameters with your own subclasses/mapped superclasses, and use doctrine:migrations:diff (never schema:update) to materialize tables.
Plan catalog is immutable and versioned. Never edit a Plan row that has subscribers — a price change silently rewrites the meaning of historical invoices. Create a new Plan (v2) and migrate subscribers explicitly, recording which plan version an invoice was billed under.
Denormalize entitlements onto the reader. The bundle resolves subscription state through the DB; feature checks run on every request. On subscription state-change, write plan_id / active_until / a limits snapshot onto your users/teams row (or a cached entitlement payload) and gate on that column. Let the billing domain be the writer; let your app be the fast reader. In Laravel, this is a listener on your SubscriptionUpdated event writing to a denormalized column, with a scheduled reconciliation job as backup.
Port the state machine before the code. Extract the explicit states and legal transitions from the source, then model them as a PHP enum plus guarded transition methods ($subscription->markPastDue(), ->cancelAtPeriodEnd()). This is the part genuinely worth borrowing — the invariants around trial expiry, mid-cycle upgrades (proration), and cancellation timing.
Replace gateway adapters with Cashier. The bundle's Gateway/ layer targets a decade-old provider API surface. Do not port it. Use Laravel Cashier (Stripe/Paddle) for the gateway, and port only the domain model and the event semantics.
Swap the event dispatcher idiomatically. Symfony's dispatcher hooks become Laravel events + queued listeners: SubscriptionCreated, SubscriptionRenewed, PaymentFailed, SubscriptionCanceled. Make anything that sends an email, writes an invoice, or calls the gateway a queued job with retries — the bundle assumes synchronous, in-request billing, which is exactly what you should not carry forward.
Webhook ingestion is your own responsibility. Build it natively: verify signatures, persist the raw event with an idempotency key, and make the handler a no-op on replay. Then reconcile — a scheduled job that compares local subscription state against the gateway's truth and repairs drift.
Seed plans, don't create them at runtime. Plans are catalog data. Use a seeder/fixture with stable slugs (pro-monthly, team-annual) that code can reference, rather than database IDs or free-text names.
DependencyInjection/Configuration.php for the real option tree. Old bundles drift badly from their docs — the config class is the source of truth for class overrides, table prefixes, and gateway credentials.migrations:diff; schema:update will silently drop columns and is a data-loss footgun.current_period_start and current_period_end, and write tests for the exact boundary instants. Porting the behavior without these invariants is how you get double charges.Gateway/ code likely calls endpoints that no longer exist. Do not "fix" it — replace it. Check which gateway the bundle assumes and confirm its current API shape before assuming any code is reusable.cache:clear) will save you an hour of reading entities.Bottom line for day-to-day: use it as a 1–2 day architectural checklist — plans, subscription states, trials, proration, invoices, dunning, cancellations — then implement the resulting model against Laravel Cashier or a small bespoke module you own and can maintain.
How can I help you explore Laravel packages today?