crumbls/subscriptions v2.0.1
Middleware
The package ships two route middleware aliases for gating access by feature or by subscription status.
can-use-feature:{slug} -- gate by feature availability
Route::middleware('can-use-feature:api-requests')->group(function () {
Route::post('/api/things', [ThingController::class, 'store']);
});
The middleware:
- Reads the authenticated user's current subscription (
auth()->user()->currentSubscription()) - Calls
$sub->canUseFeature('api-requests') - Aborts with 403 if the feature is unavailable, otherwise hands off to the controller
You can pin the check to a specific subscription slug with a second argument:
Route::middleware('can-use-feature:api-requests,pro')->group(/* ... */);
This calls $user->planSubscription('pro')->canUseFeature('api-requests') instead of currentSubscription(). Useful when a user runs multiple subscriptions and only one of them grants the feature.
subscribed[:{plan-slug}] -- gate by subscription presence
// Any active subscription
Route::middleware('subscribed')->group(/* ... */);
// Specifically subscribed to the 'pro' plan
Route::middleware('subscribed:pro')->group(/* ... */);
subscribed (no args) requires $user->hasActiveSubscription() to be true.
subscribed:pro requires the user to hold an active subscription whose plan slug is pro.
What the middleware assumes
- The route is behind the
authmiddleware (soauth()->user()is set) - The authenticated user model uses the
HasPlanSubscriptionstrait - For feature checks, the feature is
attach()ed to the relevant plan with avaluethat evaluates as enabled / non-zero
If any of these is false, the middleware short-circuits with 403 -- not a 500. The middleware never throws.
Combining
Route::middleware(['auth', 'subscribed', 'can-use-feature:premium-export'])
->post('/exports/premium', ExportController::class);
Run cheap checks first (auth, subscribed) and feature checks last so unauthorized requests don't pay for a feature lookup.
Customizing the response
The default 403 is plain. To return JSON or render a custom view, publish the middleware via composer copy:middleware... actually no -- this isn't supported out of the box. Catch the abort upstream in your exception handler:
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (HttpException $e, Request $request) {
if ($e->getStatusCode() === 403 && $request->is('api/*')) {
return response()->json(['error' => 'Subscription does not include this feature.'], 403);
}
});
});