Middleware & Blade
Tashil ships route middleware and Blade directives that gate access declaratively — no manual checks sprinkled through your controllers. Both share one pluggable resolver that decides who the current subscriber is.
Route middleware
Three middleware register automatically on boot:
| Alias | Requires |
|---|---|
subscribed | The resolved subscriber has a currently valid subscription. |
plan:{slug} | Subscribed and on the named plan. |
feature:{slug} | Subscribed and the feature check passes for the named feature. |
Route::middleware('subscribed')->group(function () {
// any valid subscription
});
Route::middleware('plan:pro')->group(function () {
// the Pro plan only
});
Route::middleware('feature:api-calls')->group(function () {
// requires the api-calls feature on the current snapshot
});All three abort with 403 on failure. The feature middleware honors per-type semantics — a
limit feature must have remaining quota, a metered feature must have sufficient balance, a boolean
feature must be truthy.
Overriding the aliases
If an alias collides with one your app already uses, rename it (or set it to null to skip
registering it):
'middleware' => [
'aliases' => [
'subscribed' => 'has-subscription', // renamed
'plan' => 'plan',
'feature' => 'feature',
],
],Blade directives
Four conditional directives gate views. They use the same resolver as the middleware, so a custom
resolver affects routes and views uniformly. All support @else, and all degrade safely to false
when no subscriber can be resolved (a guest, no override, or a resolved object that isn't
Subscribable).
@subscribed
{{-- a valid subscription exists (Active / OnTrial / grace) --}}
@else
<a href="{{ route('pricing') }}">Choose a plan</a>
@endsubscribed
@plan('pro')
{{-- on the Pro plan --}}
@endplan
@feature('api-calls')
{{-- the feature check passes --}}
@else
<p>Upgrade to unlock the API.</p>
@endfeature
@onTrial
{{-- strictly on trial: status == OnTrial AND trial_ends_at in the future --}}
@endonTrialThe subscribable resolver
By default, the "current subscriber" is auth()->user(). That's perfect for user-scoped apps. For
team- or tenant-scoped billing, override the resolver once — typically in AppServiceProvider::boot:
use Foysal50x\Tashil\Facades\Tashil;
public function boot(): void
{
Tashil::resolveSubscribableUsing(fn () => Team::current());
}Now every middleware check and Blade directive resolves the current Team instead of the user.
Tashil::resolveSubscribable() returns null when no resolver is registered and auth()->user()
is unauthenticated, or when the resolved object doesn't implement Subscribable. The middleware
treat both cases as "deny → 403"; the Blade directives treat both as false.
Reset the resolver in tests
The resolver is stored statically on the Tashil facade and persists across requests within a
process. In tests, call Tashil::forgetSubscribableResolver() in your teardown to avoid one test's
resolver bleeding into the next.
Choosing the active subscription
When a subscriber holds more than one subscription, Tashil needs to know which one represents "the
active one" for feature and lifecycle checks. By default it's the latest valid subscription. Override
resolveSubscription() on your model for multi-subscription or workspace scenarios:
use Foysal50x\Tashil\Contracts\Subscribable;
use Foysal50x\Tashil\Models\Subscription;
use Foysal50x\Tashil\Traits\HasSubscriptions;
class Team extends Model implements Subscribable
{
use HasSubscriptions;
public function resolveSubscription(): ?Subscription
{
// Prefer the subscription tied to the team's current workspace.
return $this->subscriptions()
->valid()
->where('package_id', $this->currentWorkspace->preferredPackageId())
->first();
}
}loadSubscription() calls resolveSubscription() exactly once per request lifecycle and memoizes
the result, so every feature check and the middleware all share the same row. Don't bypass the
resolver with direct repository calls — multi-subscription hosts depend on the override taking effect
uniformly.