District Core Developer DocsDistrict Core Developer Docs
Developers
Boilerplate
Modules
Bitbucket
Developers
Boilerplate
Modules
Bitbucket
  • Modules

    • ABN
    • ActivityLog
    • AnalyticsApi
    • ApiConnector
    • BlockApi
    • CategoryApi
    • CloneApi
    • CommentApi
    • ContentApi
    • Core
    • Documents
    • EmbedApi
    • Event
    • ExportApi
    • FeatureApi
    • FormApi
    • GTM
    • GalleryApi
    • HelpApi
    • Hotspot
    • IdeaSurvey
    • ImportApi
    • InteractionsApi
    • Intercom
    • MailApi
    • MapApi
    • MapSurvey
    • MediaApi
    • MenuApi
    • MetaTagApi
    • NlpApi
    • NotificationApi
    • Page
    • ParentableContent
    • PaymentApi
    • PermissionsApi
    • Postcode
    • ReCaptcha
    • Redirects
    • Renderer
    • ReportApi
    • RestrictionApi
    • RevisionApi
    • SearchApi
    • Settings
    • ShareableApi
    • Slack
    • SlugApi
    • SubscribableApi
    • Survey
    • Team
    • TenantApi
    • TestApi
    • ThemeApi
    • Timeline
    • TranslationApi
    • Update
    • Users
    • VisualisationApi
    • WorkflowApi
    • Wysiwyg

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.


Edit this page
Prev
ExportApi
Next
FormApi