Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
],
"require": {
"php": ">=8.2",
"rtcamp/wp-framework": "dev-main"
"rtcamp/wp-framework": "dev-feature/feature-selector-utility"
},
"require-dev": {
"wp-coding-standards/wpcs": "^2.3",
Expand Down
17 changes: 8 additions & 9 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions inc/Core/Features.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php
/**
* Theme feature-flags service.
*
* @package rtCamp\Theme\Elementary
*/

declare( strict_types = 1 );

namespace rtCamp\Theme\Elementary\Core;

use rtCamp\WPFramework\Contracts\Interfaces\Shareable;
use rtCamp\WPFramework\Utils\FeatureSelector;

/**
* Class Features
*
* The theme's feature-flag registry. Extends the framework FeatureSelector
* with the `elementary` context, so flag state derives as:
*
* - option key: elementary_feature_{flag} (toggled in admin under
* Settings → Features, see Modules\Settings\FeaturesSettingsPage)
* - override constant: ELEMENTARY_FEATURE_{FLAG} (wp-config.php; wins over
* the option and locks the admin checkbox)
*
* Flags default to disabled. Read them at hook time through
* Helpers\Util::is_feature_enabled(); load-time consumers (constructors,
* ConditionallyRegistrable::can_register()) must construct their own
* `new Features()` instead — Main::get_instance()->get_shared() would re-enter
* the Singleton mid-construction, since the instance is only assigned after
* Main's constructor (and thus the Loader) finishes. A fresh instance is
* equivalent to the shared one: the registry is rebuilt identically in the
* constructor and all toggle state lives in options/constants.
*
* Flag labels are intentionally not translated here: this constructor runs
* when functions.php boots Main, before the theme text domain loads. The
* settings page reads {@see Features::get_features()} lazily at `admin_init`,
* so translated labels are supplied there instead.
*
* @since 1.0.0
*/
final class Features extends FeatureSelector implements Shareable {

/**
* Flag gating the AuthorBio shortcode module.
*/
public const AUTHOR_BIO = 'author-bio';

/**
* Flag gating the MediaTextInteractive block extension.
*/
public const MEDIA_TEXT_INTERACTIVE = 'media-text-interactive';

/**
* Constructor.
*/
public function __construct() {
parent::__construct( 'elementary' );

$this->register(
[
self::AUTHOR_BIO,
self::MEDIA_TEXT_INTERACTIVE,
]
);
}

/**
* {@inheritDoc}
*
* Merges translated display labels onto the registered flags. Safe to
* translate here: this is only read lazily (the settings page calls it at
* `admin_init`), after the theme text domain has loaded.
*/
public function get_features(): array {
$labels = [
self::AUTHOR_BIO => [
'name' => __( 'Author bio shortcode', 'elementary-theme' ),
'description' => __( 'Registers the [elementary_author_bio] shortcode rendering the author-bio template part.', 'elementary-theme' ),
],
self::MEDIA_TEXT_INTERACTIVE => [
'name' => __( 'Interactive media & text', 'elementary-theme' ),
'description' => __( 'Enhances core button, columns, and video blocks with the media-text interactivity behavior.', 'elementary-theme' ),
],
];

$features = parent::get_features();

foreach ( $labels as $slug => $meta ) {
if ( isset( $features[ $slug ] ) ) {
$features[ $slug ] = array_merge( $features[ $slug ], $meta );
}
}

return $features;
}
}
23 changes: 23 additions & 0 deletions inc/Helpers/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use rtCamp\Theme\Elementary\Core\Components;
use rtCamp\Theme\Elementary\Core\Encryption;
use rtCamp\Theme\Elementary\Core\Features;
use rtCamp\Theme\Elementary\Core\Templates;
use rtCamp\Theme\Elementary\Main;

Expand Down Expand Up @@ -158,4 +159,26 @@ private static function encryptor(): Encryption {

return $encryptor;
}

/**
* Whether a theme feature flag is enabled.
*
* For use at hook time. Do not call during Main's load (constructors,
* can_register()) — the Singleton is not assigned yet; construct a
* Features instance directly there instead.
*
* @param string $flag Feature-flag slug, e.g. Features::AUTHOR_BIO.
*
* @return bool True if enabled, false otherwise.
*/
public static function is_feature_enabled( string $flag ): bool {
/**
* Shared feature-flag registry.
*
* @var Features $features
*/
$features = Main::get_instance()->get_shared( Features::class );

return $features->is_enabled( $flag );
}
}
6 changes: 4 additions & 2 deletions inc/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
namespace rtCamp\Theme\Elementary;

use rtCamp\WPFramework\Contracts\Traits\{Singleton, Loader};
use rtCamp\Theme\Elementary\Core\{Assets, Components, Encryption, Menu, Templates, ThemeSetup};
use rtCamp\Theme\Elementary\Modules\{BlockExtensions\MediaTextInteractive, Settings\ThemeOptions, Shortcodes\AuthorBio};
use rtCamp\Theme\Elementary\Core\{Assets, Components, Encryption, Features, Menu, Templates, ThemeSetup};
use rtCamp\Theme\Elementary\Modules\{BlockExtensions\MediaTextInteractive, Settings\FeaturesSettingsPage, Settings\ThemeOptions, Shortcodes\AuthorBio};

/**
* Class Main
Expand All @@ -33,8 +33,10 @@ class Main {
Components::class,
Templates::class,
Encryption::class,
Features::class,
MediaTextInteractive::class,
ThemeOptions::class,
FeaturesSettingsPage::class,
AuthorBio::class,
];

Expand Down
20 changes: 18 additions & 2 deletions inc/Modules/BlockExtensions/MediaTextInteractive.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,28 @@
namespace rtCamp\Theme\Elementary\Modules\BlockExtensions;

use WP_HTML_Tag_Processor;
use rtCamp\WPFramework\Contracts\Interfaces\Registrable;
use rtCamp\Theme\Elementary\Core\Features;
use rtCamp\WPFramework\Contracts\Interfaces\ConditionallyRegistrable;

/**
* Class MediaTextInteractive
*
* Gated behind the `media-text-interactive` feature flag (Settings →
* Features), disabled by default; toggling the flag takes effect on the next
* request, since registration is decided once at load.
*/
class MediaTextInteractive implements Registrable {
class MediaTextInteractive implements ConditionallyRegistrable {

/**
* {@inheritDoc}
*
* Runs during Main's load — Util::is_feature_enabled() / get_shared()
* would re-enter the Singleton here, so construct a Features instance
* directly (see the Features docblock for why that is equivalent).
*/
public function can_register(): bool {
return ( new Features() )->is_enabled( Features::MEDIA_TEXT_INTERACTIVE );
}

/**
* Register hooks.
Expand Down
51 changes: 51 additions & 0 deletions inc/Modules/Settings/FeaturesSettingsPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* Features settings page.
*
* @package rtCamp\Theme\Elementary\Modules\Settings
*/

declare( strict_types = 1 );

namespace rtCamp\Theme\Elementary\Modules\Settings;

use rtCamp\Theme\Elementary\Core\Features;
use rtCamp\WPFramework\Utils\FeatureSelectorSettingsPage;

/**
* Class FeaturesSettingsPage
*
* Admin UI for the theme's feature flags, at Settings → Features (slug
* `elementary-features`). The framework page renders one checkbox per flag
* registered on the injected selector and shows constant-overridden flags as
* locked — only the titles are overridden here, for the theme text domain.
*
* @since 1.0.0
*/
final class FeaturesSettingsPage extends FeatureSelectorSettingsPage {

/**
* Constructor.
*
* The Loader instantiates without arguments, so the selector cannot be
* injected; see the Features docblock for why a fresh instance is
* equivalent to the shared one.
*/
public function __construct() {
parent::__construct( new Features() );
}

/**
* {@inheritDoc}
*/
protected function get_page_title(): string {
return __( 'Elementary Features', 'elementary-theme' );
}

/**
* {@inheritDoc}
*/
protected function get_menu_title(): string {
return __( 'Features', 'elementary-theme' );
}
}
20 changes: 18 additions & 2 deletions inc/Modules/Shortcodes/AuthorBio.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

namespace rtCamp\Theme\Elementary\Modules\Shortcodes;

use rtCamp\Theme\Elementary\Core\Features;
use rtCamp\Theme\Elementary\Helpers\Util;
use rtCamp\WPFramework\Contracts\Interfaces\Registrable;
use rtCamp\WPFramework\Contracts\Interfaces\ConditionallyRegistrable;

/**
* Class AuthorBio
Expand All @@ -20,9 +21,24 @@
* template part through Util::get_template() — a child theme can override the
* markup by shipping its own template-parts/author-bio.php.
*
* Gated behind the `author-bio` feature flag (Settings → Features), disabled
* by default; toggling the flag takes effect on the next request, since
* registration is decided once at load.
*
* @since 1.0.0
*/
final class AuthorBio implements Registrable {
final class AuthorBio implements ConditionallyRegistrable {

/**
* {@inheritDoc}
*
* Runs during Main's load — Util::is_feature_enabled() / get_shared()
* would re-enter the Singleton here, so construct a Features instance
* directly (see the Features docblock for why that is equivalent).
*/
public function can_register(): bool {
return ( new Features() )->is_enabled( Features::AUTHOR_BIO );
}

/**
* Register hooks.
Expand Down
Loading