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

Tomato Subscription Laravel Package

tomatophp/tomato-subscription

Laravel subscription management for TomatoPHP, with plans, features, and subscriber billing. Includes Filament admin resources to create plans, assign subscriptions, and track renewals, all configurable and ready to drop into your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements: PHP 8.1+ and Laravel 9/10/11/12. On Laravel 11+ you must be on >= v1.1.2 — earlier versions ship a plans migration that calls the removed Blueprint::unsignedDouble() method and will abort php artisan migrate outright.

  1. composer require <vendor>/<package>
  2. Publish config and migrations:
    php artisan vendor:publish --tag=plans-config
    php artisan vendor:publish --tag=plans-migrations
    php artisan migrate
    
  3. Add the package trait (e.g. HasPlans) to your billable model — usually App\Models\User.
  4. Create your first plan and subscribe a user:
    $plan = Plan::create([
        'slug' => 'pro',
        'name' => 'Pro',
        'price' => 19.00,
        'billing_interval' => 'month',
    ]);
    
    $user->subscribeTo('pro');
    

Where to look first:

  • config/plans.php — table names, model bindings, feature definitions. Read this before touching anything else; most "why doesn't this work" issues are a model/table override here.
  • The published migration files — you own these now (see Gotchas).
  • The Plan, PlanFeature, and PlanSubscription models, plus the subscriber trait. That's the whole surface area.

First real use case: gate a feature behind a plan. Define the feature on the plan, then check it against the user's active subscription in a controller, policy, or Blade directive. Get that vertical slice working end-to-end before wiring in usage limits or plan swapping.

Implementation Patterns

Treat plans as seed data, not user data. Plans, features, and their limits are reference data. Define them in a seeder (or a dedicated PlanSeeder run from your deploy pipeline) keyed by a stable slug, and make the seeder idempotent with updateOrCreate. Never create plans ad-hoc in controllers — you'll end up with divergent plan rows across environments, and slug-based lookups will silently resolve to whichever row won the race.

Feature gating via a single choke point. Rather than scattering $user->planSubscription()->hasFeature(...) calls, wrap entitlement checks in one service (e.g. Entitlements::allows($user, 'exports')) that resolves the active subscription, falls back to a free/default plan, and handles the no-subscription case explicitly. Every route middleware, policy, and Blade check calls that service. When the package's API shifts or you add a trial concept, you change one file.

Usage-based features: record at the domain boundary, check at the edge. For metered features (API calls, seats, exports), call recordUsage() inside the domain action that consumes the resource — not in the controller — so queued jobs and console commands are metered identically. Validate against the limit before performing the work, and decide deliberately whether a rejected over-limit call still records usage (usually no). Prefer incrementing and checking in the same DB transaction where the underlying work is transactional.

Subscription lifecycle as explicit transitions. Centralise subscribeTo / swapPlan / cancel in actions or a small subscription service. Plan swaps are where bugs live: decide up front whether a downgrade should prune excess seats/features immediately or at period end, and encode that decision. If you need proration, that is your billing integration's job — this package tracks plan state, not invoices.

Route protection. A middleware alias (plan:pro or feature:exports) applied to route groups is cheaper to reason about than per-action checks, and it fails closed. Combine it with a policy for per-resource authorisation — middleware answers "does this user's plan include it", policies answer "does this user own it".

Testing. Use RefreshDatabase. Build plans in a factory or a test helper (createProPlan()) so tests don't depend on your production seeder. Write one test that asserts a free user is blocked and a pro user is allowed for each gated feature — that test suite is your regression net when you upgrade the package or refactor the entitlement service. For over-limit paths, freeze/advance time rather than sleeping, since usage windows are period-based.

Gotchas and Tips

The v1.1.2 migration fix does not reach you if you already published. This is the single most important operational detail. The fix (unsignedDouble → double()->unsigned()) only changes the package's migration stub. If you published migrations on an older version, your database/migrations/*_create_plans_table.php still contains the broken call and will keep failing on Laravel 11+ regardless of the composer update. Remedy: either hand-edit the column definition in your published file, or delete the published plans migration and re-publish before it has run in any environment (after it has run, re-publishing creates a duplicate create_plans_table and migrate dies with "table already exists"). Diff your published migrations against vendor/.../database/migrations after every minor upgrade — this class of drift is silent and cumulative.

Re-publishing is destructive and easy to do by accident. vendor:publish --tag=plans-migrations without --force may skip, and with --force overwrites your local edits. Neither behaviour is what you want mid-project. Publish once, commit the files, and treat package migrations as read-only reference thereafter.

double is not a money type. The fixed column stores a floating-point value. If it holds a price, do not compare with ==, do not accumulate with += across many rows, and do not trust 19.99 to round-trip exactly. Either store minor units (integer cents) in your own layer and keep the package column for display, or cast to a decimal string on read. Formatting belongs at the view boundary. This is a latent bug in the schema, not something the patch addresses.

Config cache. config/plans.php is read at boot. After editing table names, model bindings, or feature defaults, run php artisan config:clear (or config:cache in deploy). A stale cached config pointing at the old model class produces confusing "class not found" or empty-relation errors that look like database problems.

Deleting or renaming plans. Foreign keys from subscriptions to plans will block deletes, and soft-deleting a plan leaves subscribers pointing at something invisible. Prefer deprecating a plan: mark it inactive, keep the row, and migrate subscribers explicitly. Never repurpose an existing slug for a differently-shaped plan — historical subscriptions will retroactively mean something else.

Multi-guard / multi-model setups. If both User and Team are billable, every entitlement lookup needs an explicit subject. Implicit "current user" resolution inside your service will silently return the wrong subscription in queued jobs and console commands where there is no authenticated user. Pass the billable model in as a required argument.

Debugging checklist when a feature check returns the wrong answer, in order: (1) is there an active subscription, and does "active" include trials/grace periods? (2) does the plan row have the feature attached, or only the feature defined in config? (3) did the config cache go stale? (4) is a second subscription row shadowing the first? The package generally resolves one subscription per subscriber per name — confirm which name you're querying.

Extension points worth knowing. Model and table names are configurable, so you can subclass Plan/PlanSubscription to add columns (e.g. stripe_price_id) without forking — bind your subclasses in the config. Prefer this over editing vendor files. Keep custom columns nullable or defaulted so the package's own creation paths don't fail when they insert without them.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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