In a Laravel app using Pennant, how do the @feature Blade directive and the EnsureFeaturesAreActive middleware gate UI and routes, and what do they return?
answer
- @feature / @else / @endfeature
- @feature('name', 'value') compares rich values
- @featureany for any of several
- EnsureFeaturesAreActive::using(...)
- 400 by default, whenInactive() to change
basics
~20 s@feature('new-checkout') renders its block when the flag is active for the default scope, and @feature('name', 'value') compares a rich value. EnsureFeaturesAreActive::using('new-checkout') on a route aborts with 400 when any listed feature is inactive, unless whenInactive() sets another response.
solid answer
~30 sPennant registers Blade `if` directives. `@feature('new-checkout') ... @else ... @endfeature` calls `Feature::active()` for the default scope; with a second argument, `@feature('purchase-button', 'seafoam-green')` compares `Feature::value()` strictly, and `@elsefeature` chains variants. `@featureany(['new-checkout', 'beta'])` passes when any is active. For routes, `->middleware(EnsureFeaturesAreActive::using('new-checkout'))` builds the middleware string; the middleware loads the features, and if **any** is inactive it aborts with **400**, with a message only when `app.debug` is on. `EnsureFeaturesAreActive::whenInactive(fn ($request, $features) => ...)` in a provider swaps in your own response, such as a redirect to the old checkout or a 404.
code
html · 13 lines@feature('new-checkout')
<a href="{{ route('checkout.v2') }}">Checkout</a>
@else
<a href="{{ route('checkout') }}">Checkout</a>
@endfeature
@feature('purchase-button', 'seafoam-green')
<button class="btn-seafoam">Pay now</button>
@elsefeature('purchase-button', 'tart-orange')
<button class="btn-orange">Pay now</button>
@else
<button>Pay now</button>
@endfeaturego deeper
Recall @feature, @else and @endfeature in Blade, and that EnsureFeaturesAreActive::using() blocks a route when a feature is off.
Explain rich-value comparison with @elsefeature, the all-must-be-active rule and the default 400 response with its debug message.
Choose responses for users outside a rollout with whenInactive(), keep view and route gating consistent, and handle guests and non-user scopes.
Decide how visible unreleased routes should be and how flag-gated UI is tested and cleaned up once a rollout is complete.
## Gating the view with @feature Pennant registers its directives through Blade's `if` mechanism, so they behave like `@if` with a custom condition. For the new checkout shown to 10% of users: - `@feature('new-checkout')` checks `Feature::active('new-checkout')` for the **default scope**, the authenticated user; - `@else` renders the old checkout for everyone else; - `@endfeature` closes the block. Because a Blade `if` directive is a normal conditional, `@unlessfeature` and `@elsefeature` come with it. There is no way to pass a scope to the directive, so for a team-scoped feature either set `Feature::resolveScopeUsing()` or compute the flag in the controller and pass a boolean to the view. ## Rich values in Blade Pennant values need not be booleans. With a second argument, the directive compares the stored value **strictly**: 1. `@feature('purchase-button', 'blue-sapphire')` renders when `Feature::value('purchase-button') === 'blue-sapphire'`; 2. `@elsefeature('purchase-button', 'seafoam-green')` tries the next variant; 3. `@else` covers any other value. Remember the activity rule: a feature is **active for any value other than `false`**, so the one-argument form `@feature('purchase-button')` is true for every colour. ## @featureany `@featureany(['new-checkout', 'checkout-beta'])` renders when at least one listed feature is active, useful for a 'try the new checkout' banner shown to anyone in either programme. ## Gating routes with the middleware `Laravel\Pennant\Middleware\EnsureFeaturesAreActive` protects routes. Its static `using()` method returns the middleware string with parameters, so you attach it like any parameterised middleware: - `Route::get('/checkout/v2', ...)->middleware(EnsureFeaturesAreActive::using('new-checkout'))`; - several features, `using('new-checkout', 'saved-cards')`, must **all** be active. Inside, the middleware calls `Feature::loadMissing()` for the listed features and then `Feature::someAreInactive()` for the default scope. If any is inactive it runs `abort(400)`, a **400 Bad Request**; when `app.debug` is true, the message names the required features. Registering middleware aliases and groups belongs to the middleware topic; here the point is the behaviour. ## Customising the response A 400 is rarely what users should see. Call `EnsureFeaturesAreActive::whenInactive()` once, typically in `AppServiceProvider::boot()`: - redirect users outside the rollout to the classic checkout; - return a 404 so the new route does not reveal itself; - or return a 403 for API clients. The callback receives the request and the list of required features. It is a static setting, so it applies to every route using the middleware. ## Choosing between the two | Need | Use | |---|---| | Swap a fragment of a page | `@feature` in Blade | | Swap between rich-value variants | `@feature('name', 'value')` with `@elsefeature` | | Block a whole route for users outside the rollout | `EnsureFeaturesAreActive::using(...)` | | Decide in PHP with a scope other than the default | `Feature::for($scope)` in the controller | Both read the same stored values and the same per-request cache, so a user who sees the new checkout link in the page can also reach the new checkout route. ## Interview traps - **"The middleware returns 403."** The default is `abort(400)`; 403 is only an example of a custom response. - **"`using('a', 'b')` means either."** Every listed feature must be active. - **"The one-argument directive can tell variants apart."** Any non-false value is active; pass the value as the second argument to compare. - **"Gating the link is enough."** Users can type the URL, so gate the route with the middleware as well as the view.
- What does Laravel Pennant's EnsureFeaturesAreActive return for a guest when the feature's resolver is typed fn (User $user)?The guest's default scope is `null`, which a `User`-typed resolver cannot accept, so the feature resolves `false` and the middleware aborts with 400, or runs your `whenInactive()` callback. Put `auth` before it if the route is for signed-in users only.
- Why doesn't @feature('purchase-button') distinguish between the colour variants in Laravel Pennant?With one argument the directive calls `Feature::active()`, and any value other than `false` counts as active, so every colour passes. Use the two-argument form, `@feature('purchase-button', 'seafoam-green')`, which compares `Feature::value()` strictly.
saying these in an interview costs you the question
- EnsureFeaturesAreActive returns a 403 Forbidden by default.
- Listing several features in using() passes if any one is active.
- @feature('purchase-button') is false unless the value is exactly true.
- The @feature directive accepts a scope argument for team features.
- whenInactive() must be configured per route.