Task
In order to easily manage DesktopImprovements feature, provide a simple mechanism that allows engineers to:
easily register new featuresallow admins to enable/disable feature by editing well defined configuration- allow user to opt-in / opt-out to Desktop Improvements project
Status (2020/04/28)
Vector's Feature Manager implements the most basic parts of @polishdeveloper's original proposal. This was done because landing a Feature Manager was seen as a priority but a lot of the functionality of the one in MobileFrontend/MinervaNeue (MFE/MN) wouldn't have been used and unused code should be considered a liability. Moreover, collecting usecases and adding functionality (if necessary) should be considered part of the RfC.
I (@phuedx) have documented a couple of simplifications and improvements in T244481#6086600.
The API of the Vector Feature Manager is as follows:
interface Requirement { function getName(): string; function isMet(): bool; } interface FeatureManager { function registerSimpleRequirement( string $name, bool $condition ); function registerRequirement( Requirement $requirement ); function registerFeature( string $name, string $requirements ); function isRequirementMet( string $name ): bool; function isFeatureEnabled( string $feature ): bool;
Note well that this API can be used to implement @polishdeveloper's proposed solution below with modes being modelled as shared requirements and multi-modal features using some custom requirement, i.e.
$featureManager->registerRequirement( new MultiModalRequirement( 'amc|beta', RequestContext::getMain(), ) ); $featureManager->registerFeature( 'MultiModalFeature', [ 'amc|beta' ] );
Solution in MFE/MN
The similar work is already done in MobileFrontend extension. Although that code has couple problems that we would like to solve before porting it:
- ServiceWiring should not depend upon RequestContext::getMain() as we found out that API contexts can be different
- Too many objects provide isEnabled() method and it's difficult to understand when to use which
- Code should work as a lib, therefore it shouldn't not assume any environment/request. In order to verify if Feature is available, we need to pass IContextSource to the check method.
- There is no clear place where the FeatureManager is Initialized, it triggers on the first $mediaWikiServices->getService('FeatureManager') therefore it leads to unexpected behavior when calling hooks
- Existing solution is pretty verbose, creating single Feature requires us to define name, config variable, group. It provides getNameKey and getDescriptionKey for translations. The Feature interface should be
simple and provide what's required. For example, The feature has no notion of the ResourceLoaderModule/Styles, therefore, when implementing Feature, in one place we had to register it, and in the second place
we had to do if ($featureManager->isAvailableInCurrentContrext( $featureName ) ) { $out->addModules( ... ) }. That is bad, as it doesn't feel like FeatureManager doesn't solve any problem, we could just do
sth like if (!empty(array_filter($config->get( "wg$featureName" )) { $out->addModules(...); } and get almost the same thing.
@polishdeveloper's Original Proposal:
Provide 2 interfaces (FMS stands for FeatureManagementSystem):
- FMS/IFeature - definition of a single feature
- FMS/ISet - definition of system mode
- FMS/IUserSelectableSet set that extends ISet, and additionally can be enabled/disabled by user
and FeatureManager class that binds everything together.
The FeatureManager should provide:
Setters:
registerFeature( $name, IFeature ) to register new feature
registerSet( $name, $ISet ) to register new set
switchSetState( $setName, $newState, IContextSource ) - allow user to switch the set state (only for opt-in/opt-out states )
Getters:
hasFeature( $featureName, IContextSource ) to check if user has access to given Feature
hasSet( $setName, IContextSource ) to check if has access (granted and/or enabled by himself ) to given set
getAllForSet( $setName, IContextSource) to fetch all features assigned to given set
getFeaturesForContext( IContextSource ) to fetch all available features in current context
Additionally it should trigger a hook FeatureManagerInitialization( $featureManager ) where different extensions could register it's own Features/Sets
Built in sets:
- Stable -> the default mode for all features enabled for all users
- Loggedin -> the feature set available only for logged-in users
Additionally provide:
- DesktopImprovements -> the feature set for new DesktopImprovents project
Please, note that some sets, like LoggedIn are granted by system, and other sets like DesktopImprovements have default state that consists of enabled (stored in config) and opted_in stored in UserPreferences.
While implementing the system try to keep it as much compatible with MobileFrontend/FeatureManager as possible. Thanks to that we should move the MobileFrontend features into one system.
The initial delivery should include Legacy and Modern modes. These should be overrideable via a useskinversion URL query parameter (see T242381#5884415) in the feature dependency. The functionality to test query parameters should be available to arbitrary feature dependencies as well. For example, if you wanted to use the existing MobileFrontend feature manager to override the page issues treatment, the URL could be passed as a dependency of the MinervaPageIssuesNewTreatment feature and tested within.