-
Notifications
You must be signed in to change notification settings - Fork 441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Experiments API #3098
base: develop
Are you sure you want to change the base?
feat: Experiments API #3098
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<?php | ||
/** | ||
* WordPress Dashboard functionality for Experiments. | ||
* | ||
* @package WPGraphQL\Experimental | ||
*/ | ||
|
||
namespace WPGraphQL\Experimental; | ||
|
||
/** | ||
* Class - Admin | ||
*/ | ||
class Admin { | ||
/** | ||
* The name of the option group | ||
* | ||
* @var string | ||
*/ | ||
public static $option_group = 'graphql_experiments_settings'; | ||
|
||
/** | ||
* Initialize Admin functionality for Experiments | ||
*/ | ||
public function init(): void { | ||
$this->register_settings(); | ||
} | ||
|
||
/** | ||
* Registers the Experiments section to the WPGraphQL Settings page. | ||
*/ | ||
private function register_settings(): void { | ||
// Register the section. | ||
register_graphql_settings_section( | ||
self::$option_group, | ||
[ | ||
'title' => __( 'Experiments 🧪', 'wp-graphql' ), | ||
'desc' => __( 'WPGraphQL Experiments are experimental features that are under active development. They may change, break, or disappear at any time.', 'wp-graphql' ), | ||
] | ||
); | ||
|
||
$experiments = ExperimentRegistry::get_experiments(); | ||
|
||
// If there are no experiments to register, display a message. | ||
if ( empty( $experiments ) ) { | ||
register_graphql_settings_field( | ||
self::$option_group, | ||
[ | ||
'label' => __( 'No Experiments Available', 'wp-graphql' ), | ||
'desc' => __( 'There are no experiments available at this time.', 'wp-graphql' ), | ||
'type' => 'html', | ||
] | ||
); | ||
return; | ||
} | ||
|
||
// Register the toggle for each Experiment. | ||
$toggle_fields = []; | ||
|
||
foreach ( $experiments as $experiment ) { | ||
$config = $experiment->get_config(); | ||
|
||
$toggle_fields[] = [ | ||
'name' => $experiment->get_slug() . '_enabled', | ||
'label' => $config['title'], | ||
'desc' => $config['description'], | ||
'type' => 'checkbox', // @todo we probably want a better type callback. | ||
'default' => 'off', | ||
'value' => $experiment->is_active() ? 'on' : 'off', | ||
'disabled' => defined( 'GRAPHQL_EXPERIMENTAL_FEATURES' ), | ||
]; | ||
} | ||
|
||
register_graphql_settings_fields( | ||
self::$option_group, | ||
$toggle_fields | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
<?php | ||
/** | ||
* Abstract class for creating new experiments. | ||
* | ||
* ALL experiments should extend this class. | ||
* | ||
* @package WPGraphQL\Experimental\Experiment | ||
* @since @todo | ||
*/ | ||
|
||
namespace WPGraphQL\Experimental\Experiment; | ||
|
||
use WPGraphQL\Experimental\Admin; | ||
|
||
/** | ||
* Class - Abstract Experiment | ||
*/ | ||
abstract class AbstractExperiment { | ||
/** | ||
* The experiment unique slug. | ||
* | ||
* @var ?string | ||
*/ | ||
protected static $slug; | ||
/** | ||
* The experiment's configuration. | ||
* | ||
* @var ?array{title:string,description:string} | ||
*/ | ||
protected $config; | ||
|
||
/** | ||
* Whether the experiment is active. | ||
* | ||
* @var ?bool | ||
*/ | ||
protected $is_active; | ||
|
||
/** | ||
* Defines the experiment slug. | ||
*/ | ||
abstract protected static function slug(): string; | ||
|
||
/** | ||
* Defines the experiment configuration. | ||
* | ||
* @return array{title:string,description:string} | ||
*/ | ||
abstract protected function config(): array; | ||
|
||
/** | ||
* Initializes the experiment. | ||
* | ||
* I.e where you put your hooks. | ||
*/ | ||
abstract protected function init(): void; | ||
|
||
/** | ||
* Loads the experiment. | ||
* | ||
* @uses AbstractExperiment::init() to initialize the experiment. | ||
*/ | ||
public function load(): void { | ||
if ( ! $this->is_active() ) { | ||
return; | ||
} | ||
|
||
$this->init(); | ||
|
||
/** | ||
* Fires after the experiment is loaded. | ||
* | ||
* @param \WPGraphQL\Experimental\Experiment\AbstractExperiment $instance The experiment instance. | ||
*/ | ||
do_action( 'wp_graphql_experiment_' . $this->get_slug() . '_loaded', $this ); | ||
} | ||
|
||
/** | ||
* Gets the experiment's configuration array. | ||
* | ||
* @return array{title:string,description:string} | ||
*/ | ||
public function get_config(): array { | ||
if ( ! isset( $this->config ) ) { | ||
$this->config = $this->prepare_config(); | ||
} | ||
|
||
return $this->config; | ||
} | ||
|
||
/** | ||
* Returns the experiment's slug. | ||
* | ||
* This is static so it can be accessed outside of the class instantiation. | ||
* | ||
* @throws \Exception If the experiment is missing a slug. | ||
*/ | ||
public static function get_slug(): string { | ||
if ( ! isset( static::$slug ) ) { | ||
$slug = static::slug(); | ||
|
||
if ( empty( $slug ) ) { | ||
throw new \Exception( | ||
sprintf( | ||
/* translators: %s: The experiment's class name. */ | ||
esc_html__( 'The experiment %s is missing a slug. Ensure a valid `slug` is defined in the ::slug() method.', 'wp-graphql' ), | ||
static::class | ||
) | ||
); | ||
} | ||
|
||
static::$slug = $slug; | ||
} | ||
|
||
return static::$slug; | ||
} | ||
|
||
/** | ||
* Whether the experiment is active. | ||
*/ | ||
public function is_active(): bool { | ||
if ( isset( $this->is_active ) ) { | ||
return $this->is_active; | ||
} | ||
|
||
// See if the experiment is set via the constant. | ||
$is_active = defined( 'GRAPHQL_EXPERIMENTAL_FEATURES' ) && is_array( GRAPHQL_EXPERIMENTAL_FEATURES ) && isset( GRAPHQL_EXPERIMENTAL_FEATURES[ static::get_slug() ] ) ? (bool) GRAPHQL_EXPERIMENTAL_FEATURES[ static::get_slug() ] : null; | ||
|
||
if ( ! isset( $is_active ) ) { | ||
$setting_key = static::get_slug() . '_enabled'; | ||
|
||
$is_active = 'on' === get_graphql_setting( $setting_key, 'off', Admin::$option_group ); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per @renatonascalves other comment, this could be a good spot for a filter, e.g. if Experiment A requires Experiment B to be active. |
||
$this->is_active = $is_active; | ||
|
||
return $this->is_active; | ||
} | ||
|
||
/** | ||
* Prepares the configuration. | ||
* | ||
* @return array{title:string,description:string} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've been using it for a while. I'll probably unseal the type before final merge, but for now it makes sure nothing unnecessary is leaking in to the PR |
||
* | ||
* @throws \Exception If the experiment is missing a slug. | ||
*/ | ||
protected function prepare_config(): array { | ||
$slug = static::get_slug(); | ||
$config = $this->config(); | ||
|
||
/** | ||
* Filters the experiment configuration. | ||
* | ||
* @param array{title:string,description:string} $config The experiment configuration. | ||
* @param string $slug The experiment's slug. | ||
*/ | ||
$config = apply_filters( 'wp_graphql_experiment_config', $config, $slug ); | ||
$config = apply_filters( 'wp_graphql_experiment_' . $slug . '_config', $config ); | ||
|
||
// Validate the config. | ||
$this->validate_config( $config ); | ||
|
||
return $config; | ||
} | ||
|
||
/** | ||
* Validates the $config array, throwing an exception if it's invalid. | ||
* | ||
* @param array<string,mixed> $config The experiment configuration. | ||
* | ||
* @throws \Exception If the config is invalid. | ||
*/ | ||
protected function validate_config( array $config ): void { | ||
if ( empty( $config ) ) { | ||
throw new \Exception( | ||
sprintf( | ||
/* translators: %s: The experiment's class name. */ | ||
esc_html__( 'The experiment %s is missing a configuration. Ensure a valid `config` is defined in the ::config() method.', 'wp-graphql' ), | ||
static::class | ||
) | ||
); | ||
} | ||
|
||
if ( ! isset( $config['title'] ) || ! is_string( $config['title'] ) ) { | ||
throw new \Exception( | ||
sprintf( | ||
/* translators: %s: The experiment's class name. */ | ||
esc_html__( 'The experiment %s is missing a title in the configuration. Ensure a valid `title` is defined in the ::config() method.', 'wp-graphql' ), | ||
static::class | ||
) | ||
); | ||
} | ||
|
||
if ( ! isset( $config['description'] ) || ! is_string( $config['description'] ) ) { | ||
throw new \Exception( | ||
sprintf( | ||
/* translators: %s: The experiment's class name. */ | ||
esc_html__( 'The experiment %s is missing a description in the configuration. Ensure a valid `description` is defined in the ::config() method.', 'wp-graphql' ), | ||
static::class | ||
) | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
/** | ||
* An Example Experiment | ||
* | ||
* @package WPGraphQL\Experimental\Experiment | ||
*/ | ||
|
||
namespace WPGraphQL\Experimental\Experiment; | ||
|
||
use WPGraphQL\Experimental\Experiment\AbstractExperiment; | ||
|
||
/** | ||
* Class - TestExperiment | ||
*/ | ||
class TestExperiment extends AbstractExperiment { | ||
/** | ||
* {@inheritDoc} | ||
*/ | ||
protected static function slug(): string { | ||
return 'test_experiment'; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
protected function config(): array { | ||
return [ | ||
'title' => __( 'Test Experiment', 'wp-graphql' ), | ||
'description' => __( 'A test experiment for WPGraphQL. Registers the `RootQuery.testExperiment` field to the schema.', 'wp-graphql' ), | ||
]; | ||
} | ||
|
||
/** | ||
* Initializes the experiment. | ||
* | ||
* I.e where you put your hooks. | ||
*/ | ||
public function init(): void { | ||
add_action( 'graphql_init', [ $this, 'register_field' ] ); | ||
} | ||
|
||
/** | ||
* Registers the field for the experiment. | ||
*/ | ||
public function register_field(): void { | ||
register_graphql_field( | ||
'RootQuery', | ||
'testExperiment', | ||
[ | ||
'type' => 'String', | ||
'description' => 'A test field for the Test Experiment.', | ||
'resolve' => static function () { | ||
return 'This is a test field for the Test Experiment.'; | ||
}, | ||
] | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the phpDoc for some methods are missing
@return
. Is this intentional?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if the return type hint is identical to the signature