laravel / pennant

A simple, lightweight library for managing feature flags.

Home Page:https://laravel.com/docs/10.x/pennant

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Scope exclusive features

xwillq opened this issue · comments

My use case

I have an application with companies and users that belong to them. I want to define feature flags that control which companies have access to certain application modules and flags that control how those modules work for certain users.

Problem

Currently, the only way to do this (that I know of) is to define all feature flags for users and check user's company when resolving value for module flags:

Feature::define('module',
    fn (User $u) => in_array($u->company->type, ['...']));

Feature::define('module-behavior',
    fn (User $u) => in_array($u->role, ['...']));

However, with this approach code for changing feature flags from admin panel becomes a lot more complicated, since when user disables a module, I have to reset or edit module flag for all users from that company.

Proposed solution

When defining a feature flag, specify which scope they can be used with, and extend default scope resolver to accept name of the scope it has to resolve. This way we could have feature flags that get resolved and stored only for certain scopes, and we wouldn't need to specify scopes explicitly for those features.

@xwillq I don't fully understand the scenario you are working within, but could you apply the feature flag to the company instead of the user?

Feature::define('module', function (Company $company) {
    return in_array($company->type, ['...']);
});
Feature::for($user->company)->active('module');

Then if you disable the feature for the company it is disabled for all users within the company?

My use case is that different companies use our application, and we want to control which of them have certain features enabled. Inside those companies, we have different departments, and they must have different logic for the same action. Something like regular and simplified flow for creating invoices, while some companies don't have invoice functionality entirely.

If i change example from before to this:

Feature::define('module',
    fn (Company $c) => in_array($c->type, ['...']));

Feature::define('module-behavior',
    fn (User $u) => in_array($u->role, ['...']));

This will cause some other problems:

  1. I cannot use EnsureFeaturesAreActive middleware for both feature flags, since it uses default scope resolver, which can either resolve User or Company, but not both.
  2. I cannot use Feature::for($company)->all(), because it will pass company to resolvers of all features, and I will get TypeError.

First problem can be circumvented with defining separate middlewares for company and user level feature, but I'm not sure if second one could be solved outside of the library.

@xwillq the middleware is intentionally simple and only focus on the default scope - the same goes for the blade directive.

If you need more complex requirements I would recommend creating your own middleware...

<?php

namespace App\Http\Middleware;

use Laravel\Pennant\Feature;

class EnsureCompanyFeaturesAreActive
{
    public function handle($request, $next, ...$features)
    {
        Feature::for($request->user()->company)->loadMissing($features);

        if (Feature::for($request->user()->company)->someAreInactive($features)) {
            abort(400);
        }

        return $next($request);
    }
}

I would also recommend not using the all method but instead passing a specific scope of features you want returned. This is the documented way of retrieving multiple feature flag values.

Feature::for($company)->values(['module']);

Okay, I can understand why creating my own middleware is intended solution for my case. But for this:

I would also recommend not using the all method but instead passing a specific scope of features you want returned. This is the documented way of retrieving multiple feature flag values.

Documentation says

The values method allows the retrieval of multiple features for a given scope
[code example]
Or, you may use the all method to retrieve the values of all defined features for a given scope

So, to me it looks like documentation contradicts your last statement.

You are correct; that is present in the docs. I don't know how I missed that when I looked.

I've made a note to look at improving the way we resolve features for incompatible feature signatures. For now I would recommend using Feature::values.