Skip to content

Commit 72b4e56

Browse files
author
Mohamed Khaled
committed
Build admin settings screen
1 parent 2a1c3b4 commit 72b4e56

29 files changed

+5826
-2524
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"scripts": {
7878
"format": "phpcbf --standard=phpcs.xml.dist",
7979
"lint": "phpcs --standard=phpcs.xml.dist",
80-
"phpstan": "phpstan analyse --memory-limit=1G",
80+
"stan": "phpstan analyse --memory-limit=1G",
8181
"test": "phpunit --strict-coverage"
8282
},
8383
"scripts-descriptions": {

includes/Abstracts/Abstract_Feature.php

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,63 @@
1515
*
1616
* Provides common functionality for all features including enable/disable state.
1717
*
18+
* ## Rules for Creating Features:
19+
*
20+
* 1. **Implement load_feature_metadata()**: Return an array with 'id', 'label', and 'description'.
21+
*
22+
* 2. **Always register settings sections**: In register(), hook into 'ai_register_settings_sections'
23+
* so your feature appears in the admin UI even when disabled.
24+
*
25+
* 3. **Check is_enabled() before functional hooks**: Only register functional hooks (like actions,
26+
* filters, REST routes) if is_enabled() returns true. This allows users to disable features.
27+
*
28+
* 4. **Pass services as parameters**: Don't use global singletons or service locators. Accept
29+
* services as method parameters (e.g., Settings_Registry passed to register_settings_sections()).
30+
*
31+
* 5. **Use Provides_Settings_Section trait**: To add a settings panel, use this trait and pass
32+
* the registry as the first parameter to register_feature_settings_section().
33+
*
34+
* ## Example:
35+
*
36+
* ```php
37+
* class My_Feature extends Abstract_Feature {
38+
* use Provides_Settings_Section;
39+
*
40+
* protected function load_feature_metadata(): array {
41+
* return array(
42+
* 'id' => 'my-feature',
43+
* 'label' => __( 'My Feature', 'ai' ),
44+
* 'description' => __( 'Description of my feature.', 'ai' ),
45+
* );
46+
* }
47+
*
48+
* public function register(): void {
49+
* // Always register settings sections.
50+
* add_action( 'ai_register_settings_sections', array( $this, 'register_settings_sections' ) );
51+
*
52+
* // Only register functional hooks if enabled.
53+
* if ( ! $this->is_enabled() ) {
54+
* return;
55+
* }
56+
*
57+
* add_action( 'init', array( $this, 'my_hook' ) );
58+
* }
59+
*
60+
* public function register_settings_sections( Settings_Registry $registry ): void {
61+
* $this->register_feature_settings_section(
62+
* $registry, // Pass as parameter
63+
* 'my-feature',
64+
* __( 'My Feature', 'ai' ),
65+
* array( $this, 'render_settings' )
66+
* );
67+
* }
68+
*
69+
* public function render_settings( Settings_Toggle $toggle, Settings_Section $section ): void {
70+
* // Render settings UI.
71+
* }
72+
* }
73+
* ```
74+
*
1875
* @since 0.1.0
1976
*/
2077
abstract class Abstract_Feature implements Feature {
@@ -50,17 +107,28 @@ abstract class Abstract_Feature implements Feature {
50107
*/
51108
private $enabled = true;
52109

110+
/**
111+
* Feature toggles service.
112+
*
113+
* @since 0.1.0
114+
* @var \WordPress\AI\Admin\Settings\Feature_Toggles|null
115+
*/
116+
private $feature_toggles = null;
117+
53118
/**
54119
* Constructor.
55120
*
56121
* Loads feature metadata and initializes properties.
57122
*
58123
* @since 0.1.0
59124
*
125+
* @param \WordPress\AI\Admin\Settings\Feature_Toggles|null $feature_toggles Optional. Feature toggles service for checking enabled state.
126+
*
60127
* @throws \WordPress\AI\Exception\Invalid_Feature_Metadata_Exception If feature metadata is invalid.
61128
*/
62-
final public function __construct() {
63-
$metadata = $this->load_feature_metadata();
129+
final public function __construct( ?\WordPress\AI\Admin\Settings\Feature_Toggles $feature_toggles = null ) {
130+
$this->feature_toggles = $feature_toggles;
131+
$metadata = $this->load_feature_metadata();
64132

65133
if ( empty( $metadata['id'] ) ) {
66134
throw new Invalid_Feature_Metadata_Exception(
@@ -132,12 +200,19 @@ public function get_description(): string {
132200
/**
133201
* Checks if feature is enabled.
134202
*
203+
* Uses injected Feature_Toggles service to check persisted toggle state.
204+
* Falls back to default enabled state if service not available.
205+
*
135206
* @since 0.1.0
136207
*
137208
* @return bool True if enabled, false otherwise.
138209
*/
139210
final public function is_enabled(): bool {
140-
$enabled = $this->enabled;
211+
if ( null !== $this->feature_toggles ) {
212+
$enabled = $this->feature_toggles->is_feature_enabled( $this->id );
213+
} else {
214+
$enabled = $this->enabled;
215+
}
141216

142217
/**
143218
* Filters the enabled status for a specific feature.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
/**
3+
* Admin page controller for the AI Experiments settings screen.
4+
*
5+
* @package WordPress\AI
6+
*/
7+
8+
namespace WordPress\AI\Admin;
9+
10+
use WordPress\AI\Admin\Settings\Settings_Registry;
11+
use WordPress\AI\Admin\Settings\Settings_Section;
12+
use WordPress\AI\Admin\Settings\Settings_Toggle;
13+
14+
/**
15+
* Handles menu registration, asset loading, and fallback rendering.
16+
*
17+
* @since 0.1.0
18+
*/
19+
class Admin_Settings_Page {
20+
/**
21+
* Menu slug for the settings page.
22+
*/
23+
private const MENU_SLUG = 'ai-experiments';
24+
25+
/**
26+
* Toggle service.
27+
*
28+
* @var \WordPress\AI\Admin\Settings\Settings_Toggle
29+
*/
30+
private $toggle;
31+
32+
/**
33+
* Settings registry.
34+
*
35+
* @var \WordPress\AI\Admin\Settings\Settings_Registry
36+
*/
37+
private $registry;
38+
39+
/**
40+
* Settings page assets handler.
41+
*
42+
* @var \WordPress\AI\Admin\Settings_Page_Assets
43+
*/
44+
private $assets;
45+
46+
/**
47+
* Settings payload builder.
48+
*
49+
* @var \WordPress\AI\Admin\Settings_Payload_Builder
50+
*/
51+
private $payload_builder;
52+
53+
/**
54+
* Hook suffix returned by add_options_page.
55+
*
56+
* @var string|false
57+
*/
58+
private $hook_suffix = '';
59+
60+
/**
61+
* Constructor.
62+
*
63+
* @since 0.1.0
64+
*
65+
* @param \WordPress\AI\Admin\Settings\Settings_Toggle $toggle Toggle service.
66+
* @param \WordPress\AI\Admin\Settings\Settings_Registry $registry Settings registry.
67+
* @param \WordPress\AI\Admin\Settings_Page_Assets $assets Assets handler.
68+
* @param \WordPress\AI\Admin\Settings_Payload_Builder $payload_builder Payload builder.
69+
*/
70+
public function __construct(
71+
Settings_Toggle $toggle,
72+
Settings_Registry $registry,
73+
Settings_Page_Assets $assets,
74+
Settings_Payload_Builder $payload_builder
75+
) {
76+
$this->toggle = $toggle;
77+
$this->registry = $registry;
78+
$this->assets = $assets;
79+
$this->payload_builder = $payload_builder;
80+
}
81+
82+
/**
83+
* Registers the submenu item under Settings.
84+
*
85+
* @since 0.1.0
86+
*/
87+
public function register_menu(): void {
88+
$this->hook_suffix = add_options_page(
89+
__( 'AI Experiments', 'ai' ),
90+
__( 'AI Experiments', 'ai' ),
91+
'manage_options',
92+
self::MENU_SLUG,
93+
array( $this, 'render' )
94+
);
95+
96+
if ( ! $this->hook_suffix ) {
97+
return;
98+
}
99+
100+
// Pass hook suffix to assets handler for conditional enqueueing.
101+
$this->assets->set_hook_suffix( $this->hook_suffix );
102+
103+
add_action(
104+
'admin_enqueue_scripts',
105+
array( $this->assets, 'enqueue_assets' )
106+
);
107+
}
108+
109+
/**
110+
* Renders the settings page markup.
111+
*
112+
* @since 0.1.0
113+
*/
114+
public function render(): void {
115+
if ( ! current_user_can( 'manage_options' ) ) {
116+
wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'ai' ) );
117+
}
118+
119+
$payload = $this->payload_builder->build();
120+
?>
121+
<div class="wrap ai-experiments-settings">
122+
<h1><?php esc_html_e( 'AI Experiments', 'ai' ); ?></h1>
123+
<p class="description">
124+
<?php esc_html_e( 'Manage access to experimental AI functionality and review feature-specific settings.', 'ai' ); ?>
125+
</p>
126+
<div id="ai-experiments-settings-root" data-settings="<?php echo esc_attr( (string) wp_json_encode( $payload ) ); ?>"></div>
127+
<div class="ai-experiments-settings__fallback">
128+
<?php $this->render_sections(); ?>
129+
</div>
130+
</div>
131+
<?php
132+
}
133+
134+
/**
135+
* Renders registered sections for the fallback experience.
136+
*
137+
* @since 0.1.0
138+
*/
139+
private function render_sections(): void {
140+
foreach ( $this->registry->get_sections() as $section ) {
141+
$this->render_section( $section );
142+
}
143+
}
144+
145+
/**
146+
* Renders an individual section.
147+
*
148+
* @since 0.1.0
149+
*
150+
* @param \WordPress\AI\Admin\Settings\Settings_Section $section Section metadata.
151+
*/
152+
private function render_section( Settings_Section $section ): void {
153+
$section_id = $section->get_id();
154+
?>
155+
<section
156+
id="ai-experiments-section-<?php echo esc_attr( $section_id ); ?>"
157+
class="ai-experiments-settings__section"
158+
>
159+
<h2><?php echo esc_html( $section->get_title() ); ?></h2>
160+
<?php if ( $section->get_description() ) : ?>
161+
<p><?php echo esc_html( $section->get_description() ); ?></p>
162+
<?php endif; ?>
163+
<div class="ai-experiments-settings__section-content">
164+
<?php call_user_func( $section->get_render_callback(), $this->toggle, $section ); ?>
165+
</div>
166+
</section>
167+
<?php
168+
}
169+
}

0 commit comments

Comments
 (0)