Skip to content

Commit

Permalink
feat: scaffold Experiments API
Browse files Browse the repository at this point in the history
  • Loading branch information
justlevine committed Apr 27, 2024
1 parent a66f3e1 commit c5ec8ed
Show file tree
Hide file tree
Showing 7 changed files with 662 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/Experimental/Admin.php
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
);
}
}
204 changes: 204 additions & 0 deletions src/Experimental/Experiment/AbstractExperiment.php
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 static $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 );
}

$this->is_active = $is_active;

return $this->is_active;
}

/**
* Prepares the configuration.
*
* @return array{title:string,description:string}
*
* @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
)
);
}
}
}
58 changes: 58 additions & 0 deletions src/Experimental/Experiment/TestExperiment.php
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.';
},
]
);
}
}

0 comments on commit c5ec8ed

Please sign in to comment.