Feature Flags Middleware
Feature flag middleware controls access to features at different organizational levels: advisor, organization, and setting-based disabling. This enables gradual rollouts and per-customer feature customization.
Middleware Aliases
Section titled “Middleware Aliases”| Alias | Class | Scope |
|---|---|---|
feature | Feature | Advisor-level features |
organization.feature | OrganizationFeature | Organization-level features |
disable_features_by_advisor_setting | DisableFeaturesByAdvisorSetting | Setting-based feature disabling |
payment.check | CheckIfPaymentRequired | License/payment validation |
1. Feature (Advisor-Level)
Section titled “1. Feature (Advisor-Level)”File: app/Http/Middleware/Feature.php
Checks if a feature is enabled for the context advisor (not login user).
Implementation
Section titled “Implementation”/** * Check whether the context (as opposed to login) advisor has certain feature enabled * * If the login user is a client, will check their advisor instead */final class Feature{ public function handle(Request $request, Closure $next, string $feature): Response { $locality = LocalityManager::locality();
if ($locality instanceof AdvisorLocality) { $advisor = $locality->getAdvisorOrFail(); } else { // team and member controller $advisor = Auth::getUserOrFail()->advisor;
throw_unless($advisor !== null); }
if (($advisor->features[$feature] ?? false) !== true) { throw new AccessDeniedHttpException( 'The ' . str_replace('_', ' ', $feature) . ' feature is not enabled for this advisor.' ); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
LocalityManager::locality()to get context - If
AdvisorLocality: gets advisor from locality - Otherwise: gets advisor from
Auth::getUserOrFail()->advisor(for team/member controllers) - Checks
$advisor->features[$feature]fortruevalue - Underscores in feature name replaced with spaces in error message
Route Usage
Section titled “Route Usage”Route::group(['middleware' => 'feature:risk'], function () { Route::get('risk-analysis', [RiskController::class, 'show']);});
Route::group(['middleware' => 'feature:tax_planning'], function () { Route::resource('tax-scenarios', TaxScenarioController::class);});Error Response
Section titled “Error Response”{ "message": "The tax planning feature is not enabled for this advisor."}HTTP Status: 403 Forbidden
2. OrganizationFeature
Section titled “2. OrganizationFeature”File: app/Http/Middleware/OrganizationFeature.php
Checks if a feature is enabled at the organization level.
Implementation
Section titled “Implementation”/** * Check whether the context (as opposed to login) organization has certain organization feature enabled */final class OrganizationFeature{ public function handle(Request $request, Closure $next, string $feature): Response { $locality = LocalityManager::locality();
if ($locality instanceof OrganizationLocality) { $organization = $locality->getOrganizationOrFail(); } elseif ($locality instanceof AdvisorLocality) { $organization = $locality->getAdvisorOrFail()->organization;
if ($organization === null) { throw new RuntimeException('Login advisor must belong to an organization.'); } } else { throw new AccessDeniedHttpException( 'The ' . $locality::class . ' locality is not supported by this route.' ); }
$organization->loadMissing(Organization::RELATION_BILLING_SUBSCRIPTIONS);
if (($organization->features[$feature] ?? false) !== true) { throw new AccessDeniedHttpException( 'The ' . str_replace('_', ' ', $feature) . ' feature is not enabled for this organization.' ); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Supports two locality types:
OrganizationLocality: gets organization directlyAdvisorLocality: gets organization via advisor (throws if advisor has no organization)
- Other locality types throw
AccessDeniedHttpException - Eager loads billing subscriptions via
loadMissing(Organization::RELATION_BILLING_SUBSCRIPTIONS) - Checks
$organization->features[$feature]fortruevalue
Route Usage
Section titled “Route Usage”Route::group([ 'middleware' => 'organization.feature:risk_management'], function () { Route::get('org/risk-dashboard', [OrgRiskController::class, 'dashboard']);});
Route::group([ 'middleware' => 'organization.feature:sso'], function () { Route::get('org/sso/settings', [SsoController::class, 'show']);});Error Responses
Section titled “Error Responses”// Feature not enabled{ "message": "The risk management feature is not enabled for this organization."}
// Unsupported locality{ "message": "The App\\Locality\\HouseholdLocality locality is not supported by this route."}
// Advisor without organizationRuntimeException: "Login advisor must belong to an organization."HTTP Status: 403 Forbidden
3. DisableFeaturesByAdvisorSetting
Section titled “3. DisableFeaturesByAdvisorSetting”File: app/Http/Middleware/DisableFeaturesByAdvisorSetting.php
Disables features based on advisor settings. Only supports specific settings via match statement.
Implementation
Section titled “Implementation”final class DisableFeaturesByAdvisorSetting{ public function handle(Request $request, Closure $next, string $setting): Response { $advisor = LocalityManager::advisorLocality()->getAdvisorOrFail();
$error_msg = match ($setting) { Advisor::SETTINGS_DISABLE_VAULT => 'The vault feature is not enabled for this advisor.', Advisor::SETTINGS_DISABLE_VAULT_SHARING => 'The vault share feature is not enabled for this advisor.', default => throw new InvalidArgumentException("Setting [$setting] is not supported."), };
if (($advisor->settings[$setting] ?? false) === true) { throw new AccessDeniedHttpException($error_msg); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
LocalityManager::advisorLocality()->getAdvisorOrFail()(specific method, notlocality()) - Only supports TWO settings via match statement:
Advisor::SETTINGS_DISABLE_VAULTAdvisor::SETTINGS_DISABLE_VAULT_SHARING
- Throws
InvalidArgumentExceptionfor unsupported settings - Checks
$advisor->settings[$setting]fortruevalue (disabled)
Supported Settings
Section titled “Supported Settings”| Constant | Value | Error Message |
|---|---|---|
Advisor::SETTINGS_DISABLE_VAULT | disable_vault | ”The vault feature is not enabled for this advisor.” |
Advisor::SETTINGS_DISABLE_VAULT_SHARING | disable_vault_sharing | ”The vault share feature is not enabled for this advisor.” |
Route Usage
Section titled “Route Usage”// Vault features (can be disabled)Route::group([ 'middleware' => 'disable_features_by_advisor_setting:disable_vault'], function () { Route::resource('vault', VaultController::class);
// Nested: vault sharing (can also be disabled separately) Route::group([ 'middleware' => 'disable_features_by_advisor_setting:disable_vault_sharing' ], function () { Route::post('vault/{document}/share', [VaultShareController::class, 'store']); });});Error Responses
Section titled “Error Responses”// Vault disabled{ "message": "The vault feature is not enabled for this advisor."}
// Vault sharing disabled{ "message": "The vault share feature is not enabled for this advisor."}HTTP Status: 403 Forbidden
For unsupported settings:
InvalidArgumentException: Setting [unknown_setting] is not supported.4. CheckIfPaymentRequired
Section titled “4. CheckIfPaymentRequired”File: app/Http/Middleware/CheckIfPaymentRequired.php
Validates that the advisor has a valid license via the model’s checkLicenseStatus() method.
Implementation
Section titled “Implementation”final class CheckIfPaymentRequired{ public function handle(Request $request, Closure $next): Response { $advisor_id = Auth::getUserOrFail()->advisor_user_id;
throw_unless($advisor_id !== null);
Advisor::findOrFail($advisor_id)->checkLicenseStatus();
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
Auth::getUserOrFail()->advisor_user_id(NOT LocalityManager) - Throws if
advisor_user_idis null - Delegates to
Advisor::checkLicenseStatus()method for actual validation - The
checkLicenseStatus()method throws appropriate exceptions if license is invalid
Route Usage
Section titled “Route Usage”Route::group(['middleware' => 'payment.check'], function () { Route::resource('advanced-reports', AdvancedReportController::class); Route::post('export/pdf', [ExportController::class, 'pdf']);});Error Response
Section titled “Error Response”Depends on Advisor::checkLicenseStatus() implementation, typically:
HTTP Status: 402 Payment Required or 403 Forbidden
5. Feature Flag Hierarchy
Section titled “5. Feature Flag Hierarchy”Organization Feature (org-level) │ ▼Advisor Feature (advisor-level) │ ▼Advisor Setting (disable override) │ ▼Payment Check (license status)Combining Feature Checks
Section titled “Combining Feature Checks”Route::group([ 'middleware' => [ 'organization.feature:risk_management', 'feature:risk', 'payment.check', ]], function () { // Requires: // 1. Organization has risk_management feature // 2. Advisor has risk feature enabled // 3. Advisor has valid license
Route::resource('risk-assessments', RiskAssessmentController::class);});6. Feature Flag Storage
Section titled “6. Feature Flag Storage”Advisor Features (JSON column)
Section titled “Advisor Features (JSON column)”// Database: advisors.features (JSON){ "risk": true, "tax_planning": true, "vault": true}
// Access in middlewareif (($advisor->features[$feature] ?? false) !== true) { ... }Organization Features
Section titled “Organization Features”// Database: organizations.features (JSON){ "risk_management": true, "compliance": true, "sso": true}
// Access in middlewareif (($organization->features[$feature] ?? false) !== true) { ... }Advisor Settings
Section titled “Advisor Settings”// Database: advisors.settings (JSON){ "disable_vault": false, "disable_vault_sharing": true}
// Access in middlewareif (($advisor->settings[$setting] ?? false) === true) { ... }7. Feature Decision Flow
Section titled “7. Feature Decision Flow”Request │ ▼┌────────────────────────┐│ Organization Feature? │────No────▶ 403│ (if middleware applied)│└────────────────────────┘ │ Yes ▼┌────────────────────────┐│ Advisor Feature? │────No────▶ 403│ (if middleware applied)│└────────────────────────┘ │ Yes ▼┌────────────────────────┐│ Disabled by Setting? │────Yes───▶ 403│ (vault/vault_sharing) │└────────────────────────┘ │ No ▼┌────────────────────────┐│ License Valid? │────No────▶ 402/403│ (checkLicenseStatus) │└────────────────────────┘ │ Yes ▼ ContinueSummary
Section titled “Summary”| Middleware | Level | Logic | Source |
|---|---|---|---|
feature:X | Advisor | Check $advisor->features[X] === true | LocalityManager or Auth |
organization.feature:X | Organization | Check $organization->features[X] === true | LocalityManager (OrganizationLocality or AdvisorLocality) |
disable_features_by_advisor_setting:X | Advisor | Block if $advisor->settings[X] === true | LocalityManager::advisorLocality() |
payment.check | Advisor | Delegate to checkLicenseStatus() | Auth::getUserOrFail()->advisor_user_id |