FeatureAPI Module
This module is responsible for toggling features in the application. It leverages Feature flags package, checkout the readme for a high level overview.
Feature storage
See Config/features.php for default pipelines. The default is ['tenant'] which is a custom gateway that checks the tenant model for feature settings. This is trumped by global feature restrictions that are stored in config('features.global_restrictions').
Features are enabled/available by default
We add restrictions to prevent access.
Global restrictions
Global restrictions will trump any tenant restrictions. Eg if you want to prevent access to MFA for all tenants, you would add this as a global restriction in Config/features.php
'global_restrictions' => [
['name' => MyFeatures::DO_A_THING 'disabled' => true],
['name' => MyFeatures::DO_A_THING_WITH_LIMIT 'limit' => 10],
],
Tenant restrictions
Tenant restrictions are defined on the tenant model and stored in the $tenant->features attribute. There is helpers for getting and setting features but the storage format is the same as global restrictions.
$tenant->features = [
['name' => MyFeatures::DO_A_THING 'disabled' => true],
['name' => MyFeatures::DO_A_THING_WITH_LIMIT 'limit' => 10],
]
Look at ModelDefinesFeatureRestrictions for helper methods
Eg.
// Get the restriction (null if not exists).
$tenant->getFeatureRestriction(MyFeatures::DO_A_THING);
// Can access a feature (is it enabled).
$tenant->canAccessFeature(MyFeatures::DO_A_THING);
// Disable restriction.
$tenant->setFeatureRestriction(MyFeatures::DO_A_THING, true);
$tenant->setFeatureRestriction(MyFeatures::DO_A_THING_WITH_LIMIT, false, 10);
$tenant->setFeatureRestriction(MyFeatures::DO_A_THING_WITH_LIMIT, limit: 10);
// Delete feature restriction.
$tenant->deleteFeatureRestriction(MyFeatures::DO_A_THING)
If a tenant is initialized you can also just use the Feature facade or FeatureService. Note these don't provide a way of setting a limit, just turning on/off.
// Facade.
Features::accessible(MyFeatures::DO_A_THING)
Features::turnOn(MyFeatures::DO_A_THING);
Features::turnOff(MyFeatures::DO_A_THING);
// Service.
FeatureService::accessible(MyFeatures::DO_A_THING)
FeatureService::turnOn(MyFeatures::DO_A_THING);
FeatureService::turnOff(MyFeatures::DO_A_THING);
Feature definition plugins
Features should all be defined in code, You can do this by simply creating a new class that lives in Modules\MyModule\Plugins\Features\Plugins and ensure it extends Modules\FeatureApi\Plugins\Features\BaseFeatureDefinition. IT should define the features as Enum/Constants and have a Description attribute to give it a human name.
Eg.
namespace Modules\MyModule\Plugins\Features\Plugins;
use BenSampo\Enum\Attributes\Description;
use Modules\FeatureApi\Plugins\Features\BaseFeatureDefinition;
class MyFeatures extends BaseFeatureDefinition
{
#[Description('Allow to do a thing')]
const DO_A_THING = 'do_a_thing';
}
Defining if the definition doesn't use limits.
Add the php attribute #[DoesNotEnforceLimit] to indicate the definition doesn't have limits, this will remove the limit field from the UI for feature selection. (Eg MFA would not have a limit)
class MyFeatures extends BaseFeatureDefinition
{
#[Description('Allow to do a thing')]
#[DoesNotEnforceLimit]
const DO_A_THING = 'do_a_thing';
}
Defining how to get the count of items.
Add the php attribute #[LimitHandler(new MyCustomLimitHandler)]] to define the class that gets the current count of usage that should be compared against the defined limit.
Eg.
class MyFeatures extends BaseFeatureDefinition
{
#[Description('Allow to do a thing')]
#[LimitHandler(new MyCustomLimitHandler)]
const DO_A_THING_WITH_LIMIT = 'do_a_thing_with_limit';
}
Then your limit handler class. This should be an invokable class that returns an int. The invoke method is passed $value which using the above example would be do_a_thing
namespace Modules\Users\Plugins\Features\Handlers;
use Modules\FeatureApi\Plugins\Features\LimitHandlerInterface;
class AdminUserCountHandler implements LimitHandlerInterface
{
public function __invoke(string $value): int
{
return DB::table('custom_table')->count();
}
}
Using a feature
Featur
Usage is abstracted via FeatureService to both simplify and ensure that the feature-flag package is not directly accessed.
Toggling features
PHP
use Modules\FeatureApi\Services\FeatureService;
use Modules\MyModule\Plugins\Features\Plugins\MyFeatures;
FeatureService::turnOn(MyFeatures::DO_A_THING);
FeatureService::turnOff(MyFeatures::DO_A_THING);
CLI
php artisan feature:on database my_feature
php artisan feature:off database my_feature
Checking enabled
Programmatically
use Modules\FeatureApi\Services\FeatureService;
if (FeatureService::accessible(MyFeatures::DO_A_THING)) {
echo 'enabled';
}
Middleware
Route::get('/', 'SomeController@get')->middleware('feature:'.MyFeatures::DO_A_THING)
Route::get('/', 'SomeController@get')->middleware('feature:'.MyFeatures::DO_A_THING.',on')
Route::get('/', 'SomeController@get')->middleware('feature:'.MyFeatures::DO_A_THING.',off,404')
Validation
Validator::make([
'name' => 'Peter',
'place' => 'England',
'email' => 'peter.fox@ylsideas.co',
], [
'name' => 'requiredWithFeature:'.MyFeatures::DO_A_THING, // required
'place' => 'requiredWithFeature:'.MyFeatures::DO_A_THING.',on', // required
'email' => 'requiredWithFeature:'.MyFeatures::DO_A_THING.',off', // not required
]);
Feature service
getAllDefinitions and getAllDefinitionsCached
Return array of features keyed by feature name, value is FeatureDefinitionDto that returns an object with getKey, getName, getTitle, getPlugin and getLimit methods.
$all = FeatureService::new()->getAllDefinitions();
$allCached = FeatureService::new()->getAllDefinitionsCached();
findByName
Return a FeatureDefinitionDto for a single feature by name.
Note: when obtaining a feature this way, it will not be hydrated with settings, it will be disabled by default with no limit
$humanTitle = FeatureService::findByName(MyFeatures::DO_A_THING)->getTitle();
Feature DTO
The FeatureDefinitionDto object exposes turnOn, turnOff and accessible methods so once loaded you can just act on that object. Eg
$feature = FeatureService::findByName(self::FEATURE_NAME);
$feature->turnOn();
$feature->turnOff();
if ($feature->accessible()) {
print $feature->getTitle() . ' is enabled'
}
Deprecated: Using DB features table for storage
This is the default for the features package but is not ideal for our workflow. So instead we have features enabled by default, then restricted by global (config file), then tenant attribute.
TODO: remove features table.