wwwision / neos-features
Feature System for Neos including backend module to manage feature states
Package info
github.com/bwaidelich/Wwwision.Neos.Features
Type:neos-package
pkg:composer/wwwision/neos-features
Requires
- php: >=8.4
- neos/neos: ^8.3
- webmozart/assert: ^1 || ^2
- wwwision/neos-modulecomponents: ^1
- wwwision/types: ^1.10
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3
- phpstan/phpstan: ^2
- phpunit/phpunit: ^12
- roave/security-advisories: dev-latest
README
Neos package providing a feature system including a backend module to activate, configure and deactivate global features of an installation
How it works
- Features are declared in
Settings.yamlwith an id, name, description, icon and optional group and dependencies - Each feature is backed by a PHP implementation that reacts to the feature lifecycle (
activate/updateOptions/deactivate) – for example by writing overrides to the dedicatedSettings.Features.yamlandNodeTypes.Features.yamlconfiguration files - Feature states (active flag and configured options) are stored in a YAML file underneath the
Data/folder - Features can depend on each other: dependencies are activated along with a feature (in the correct order) and a feature cannot be deactivated while active features still depend on it
- Relevant caches are flushed automatically whenever a feature state changes
Usage
- Install the package via composer:
composer require wwwision/neos-features
- Grant access to the backend module to the corresponding roles via
Policy.yaml:
roles: 'Neos.Neos:Administrator': privileges: - privilegeTarget: 'Wwwision.Neos.Features.Module:FeaturesModule' permission: GRANT
- Declare a first feature via
Settings.yaml:
Wwwision: Neos: Features: features: 'dark-mode': name: 'Dark mode' description: 'Enables the dark theme in the site frontend' icon: 'moon'
Note
A feature that declares neither an objectName nor a factoryClassName is a noop feature: activating it merely records the state without further side effects.
See Feature implementations below for features that actually do something.
- Navigate to the new backend module
Log in as Neos administrator and navigate to the new "Features" module underneath the "Administration" main module, or head straight to /neos/administration/features
Declaring features
Features and feature groups are declared underneath the Wwwision.Neos.Features settings:
Wwwision: Neos: Features: featureGroups: 'marketing': name: 'Marketing' description: 'Marketing & tracking related features' icon: 'bullhorn' features: 'maintenance-mode': name: 'Maintenance mode' description: 'Shows a maintenance page to visitors' icon: 'wrench' objectName: 'Some\Package\Features\MaintenanceModeFeature' 'testimonials': name: 'Testimonial documents' description: 'Allows editors to create testimonial pages' icon: 'quote-right' group: 'marketing' factoryClassName: 'Wwwision\Neos\Features\Model\CommonFeatures\ActivateNodeTypeFeatureFactory' options: nodeType: 'Some.Package:Document.Testimonial' 'testimonial-carousel': name: 'Testimonial carousel' description: 'Renders a carousel of testimonials on the homepage' group: 'marketing' dependsOn: ['testimonials']
Every feature supports the following settings:
name– label displayed in the backend module (defaults to the feature id)description– optional description displayed in the backend moduleicon– optional Font Awesome icon namegroup– optional id of a group declared underneathfeatureGroupsdependsOn– optional list of feature ids this feature depends onposition– optional position of the feature within its group, supporting the usualstart/end/before <id>/after <id>syntaxobjectName– optional class name of a feature implementationfactoryClassName– optional class name of a feature factory (mutually exclusive withobjectName)options– optional factory options, only allowed in combination withfactoryClassName
The dependency graph is validated eagerly: unknown dependsOn references, dependency cycles and unknown group references all fail at load time with a corresponding exception.
Feature implementations
The behavior of a feature is provided by a PHP class implementing one of two interfaces:
OptionlessFeatureImplementation– for features without configuration:activate(context)/deactivate(context)ConfigurableFeatureImplementation– for features with typed options:activate(context, options)/updateOptions(context, previous, new)/deactivate(context, previousOptions)
Optionless features
The following example enables/disables a (fictional) dark mode setting of the site package by writing to the shared settings file:
<?php declare(strict_types=1); namespace Some\Package\Features; use Wwwision\Neos\Features\Model\Feature\FeatureActivateResult; use Wwwision\Neos\Features\Model\Feature\FeatureDeactivateResult; use Wwwision\Neos\Features\Model\FeatureImplementation\FeatureContext; use Wwwision\Neos\Features\Model\FeatureImplementation\OptionlessFeatureImplementation; final readonly class DarkModeFeature implements OptionlessFeatureImplementation { public function activate(FeatureContext $context): FeatureActivateResult { $context->settingsFile()->set(['Some', 'Package', 'darkMode', 'enabled'], true); return FeatureActivateResult::success(); } public function deactivate(FeatureContext $context): FeatureDeactivateResult { $context->settingsFile()->unset(['Some', 'Package', 'darkMode', 'enabled']); return FeatureDeactivateResult::success(); } }
Configurable features
A configurable feature declares a class implementing the FeatureOptions marker interface. Its constructor signature defines the schema of the activation/update form in the backend module (rendered via wwwision/types):
<?php declare(strict_types=1); namespace Some\Package\Features; use Neos\Flow\Annotations as Flow; use Wwwision\Neos\Features\Model\Feature\FeatureOptions; #[Flow\Proxy(false)] final readonly class MaintenanceModeOptions implements FeatureOptions { public function __construct( public string $message, public bool $allowBackendUsers = true, ) {} }
Important
Options classes have to be annotated with #[Flow\Proxy(false)] – otherwise Flow will replace them with a generated proxy class, breaking the reflection-based extraction of the options schema that the form rendering and (de)serialization rely on
The corresponding implementation receives the configured options in its lifecycle methods:
<?php declare(strict_types=1); namespace Some\Package\Features; use Wwwision\Neos\Features\Model\Feature\FeatureActivateResult; use Wwwision\Neos\Features\Model\Feature\FeatureDeactivateResult; use Wwwision\Neos\Features\Model\Feature\FeatureOptions; use Wwwision\Neos\Features\Model\Feature\FeatureUpdateOptionsResult; use Wwwision\Neos\Features\Model\FeatureImplementation\ConfigurableFeatureImplementation; use Wwwision\Neos\Features\Model\FeatureImplementation\FeatureContext; /** * @implements ConfigurableFeatureImplementation<MaintenanceModeOptions> */ final readonly class MaintenanceModeFeature implements ConfigurableFeatureImplementation { public static function optionsClassName(): string { return MaintenanceModeOptions::class; } public function activate(FeatureContext $context, FeatureOptions $options): FeatureActivateResult { assert($options instanceof MaintenanceModeOptions); $context->settingsFile()->setMany([ [['Some', 'Package', 'maintenance', 'enabled'], true], [['Some', 'Package', 'maintenance', 'message'], $options->message], [['Some', 'Package', 'maintenance', 'allowBackendUsers'], $options->allowBackendUsers], ]); return FeatureActivateResult::success(); } public function updateOptions(FeatureContext $context, FeatureOptions $previousOptions, FeatureOptions $newOptions): FeatureUpdateOptionsResult { assert($newOptions instanceof MaintenanceModeOptions); $context->settingsFile()->setMany([ [['Some', 'Package', 'maintenance', 'message'], $newOptions->message], [['Some', 'Package', 'maintenance', 'allowBackendUsers'], $newOptions->allowBackendUsers], ]); return FeatureUpdateOptionsResult::success(); } public function deactivate(FeatureContext $context, FeatureOptions $previousOptions): FeatureDeactivateResult { $context->settingsFile()->unset(['Some', 'Package', 'maintenance']); return FeatureDeactivateResult::success(); } }
Common option types
Options of type string, int and bool are rendered as text, number and checkbox inputs respectively.
For more specialized editors, this package provides a set of value objects in the Wwwision\Neos\Features\Model\CommonOptions namespace that can be used as option types:
Date– date pickerDateAndTimeLocal– date & time pickerEmailAddress– email inputPassword– password inputUrl– URL inputFile– file uploadImageFile– image upload
#[Flow\Proxy(false)] final readonly class NewsletterOptions implements FeatureOptions { public function __construct( public string $apiKey, public EmailAddress $senderAddress, public Url|null $termsUrl = null, ) {} }
Feature factories
A feature factory builds a feature implementation from static factory options, allowing a single implementation to be reused across multiple features with different parameters. It is bound to a feature via the factoryClassName setting (see Declaring features) and has to implement the FeatureImplementationFactory interface:
<?php declare(strict_types=1); namespace Some\Package\Features; use Webmozart\Assert\Assert; use Wwwision\Neos\Features\Model\FeatureImplementation\FeatureImplementation; use Wwwision\Neos\Features\Model\FeatureImplementation\FeatureImplementationFactory; final readonly class RedirectFeatureFactory implements FeatureImplementationFactory { public function create(array $options): FeatureImplementation { Assert::string($options['targetUri'] ?? null, 'RedirectFeature requires a "targetUri" factory option'); return new RedirectFeature($options['targetUri']); } }
Note
Factory options are static build-time parameters declared in Settings.yaml and invisible to the editor – not to be confused with the activation-time options collected via the backend module
Built-in: ActivateNodeTypeFeature
The package ships a reusable ActivateNodeTypeFeatureFactory that makes one or more (otherwise abstract) node types available by writing abstract: false overrides to the shared NodeTypes file – see the "testimonials" feature in Declaring features above. Multiple node types can be specified via the nodeTypes (instead of the nodeType) factory option.
The feature context
Every lifecycle method receives a FeatureContext as its first argument, providing access to the two shared YAML configuration files and helpers for invoking Flow CLI commands:
// write/remove nested values in Settings.Features.yaml and NodeTypes.Features.yaml: $context->settingsFile()->set(['Some', 'Package', 'foo'], 'bar'); $context->nodeTypesFile()->unset(['Some.Package:Document.Testimonial']); // mark node types as non-abstract (and revert that): $context->activateNodeTypes('Some.Package:Document.Testimonial'); $context->deactivateNodeTypes('Some.Package:Document.Testimonial'); // run Flow CLI commands in a sub-process (synchronously or asynchronously): $context->executeCommand('some.package:search:buildindex'); $context->executeCommandAsync('some.package:search:buildindex');
Important
Make sure that the two configuration files are actually evaluated by Flow/Neos, i.e. that your installation includes Settings.Features.yaml and NodeTypes.Features.yaml files from the Configuration folder (this is the case by default for Flow 8+ / Neos 8+ setups)
Checking feature states in PHP
The central FeatureSystem can be injected in order to interact with features programmatically, for example to check whether a feature is currently active:
use Wwwision\Neos\Features\FeatureSystem; use Wwwision\Neos\Features\Model\Feature\FeatureId; // e.g. in a service or controller with an injected $featureSystem: $feature = $this->featureSystem->getFeature(FeatureId::fromString('dark-mode')); if ($feature->active) { // ... }
Tip
In most cases this is not needed: since feature implementations write regular Flow configuration, the site package can simply react to the corresponding settings and node types
Configuration
The paths of the involved YAML files can be adjusted via Settings.yaml, the defaults are:
Wwwision: Neos: Features: states: path: '%FLOW_PATH_DATA%Features/FeatureStates.yaml' configurationFiles: settings: path: '%FLOW_PATH_CONFIGURATION%Settings.Features.yaml' nodeTypes: path: '%FLOW_PATH_CONFIGURATION%NodeTypes.Features.yaml'
Contribution
Contributions in the form of issues or pull requests are highly appreciated.
License
See LICENSE
