diff --git a/composer.json b/composer.json
index ac8a38e2..cfa3923b 100644
--- a/composer.json
+++ b/composer.json
@@ -59,10 +59,16 @@
"admin/",
"includes/classes/",
"includes/deprecated/"
- ]
+ ],
+ "psr-4": {
+ "EqualizeDigital\\AccessibilityChecker\\": "includes/classes/"
+ }
},
"autoload-dev": {
- "classmap": []
+ "classmap": [],
+ "psr-4": {
+ "EqualizeDigital\\AccessibilityChecker\\Tests\\TestHelpers\\": "tests/phpunit/TestHelpers/"
+ }
},
"scripts": {
"lint": [
diff --git a/includes/classes/WPCLI/BootstrapCLI.php b/includes/classes/WPCLI/BootstrapCLI.php
new file mode 100644
index 00000000..35e9f11b
--- /dev/null
+++ b/includes/classes/WPCLI/BootstrapCLI.php
@@ -0,0 +1,107 @@
+wp_cli = $wp_cli ? $wp_cli : new WP_CLI();
+ }
+
+ /**
+ * Register the WP-CLI commands by looping through the commands array and adding each command.
+ *
+ * @since 1.15.0
+ *
+ * @return void
+ */
+ public function register() {
+ // Bail if not running in WP_CLI.
+ if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
+ return;
+ }
+
+ /**
+ * Filter the list of classes that hold the commands to be registered.
+ *
+ * @since 1.15.0
+ *
+ * @param CLICommandInterface[] $commands array of classes to register as commands.
+ */
+ $commands = apply_filters( 'edac_filter_command_classes', $this->commands );
+
+ foreach ( $commands as $command ) {
+ // All commands must follow the interface.
+ if ( ! is_subclass_of( $command, CLICommandInterface::class, true ) ) {
+ continue;
+ }
+
+ try {
+ $this->wp_cli::add_command(
+ $command::get_name(),
+ $command,
+ $command::get_args()
+ );
+ } catch ( Exception $e ) {
+ $this->wp_cli::warning(
+ sprintf(
+ // translators: 1: a php classname, 2: an error message that was thrown about why this failed to register.
+ esc_html__( 'Failed to register command %1$s because %2$s', 'accessibility-checker' ),
+ $command,
+ $e->getMessage()
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/includes/classes/WPCLI/Command/CLICommandInterface.php b/includes/classes/WPCLI/Command/CLICommandInterface.php
new file mode 100644
index 00000000..814d7d01
--- /dev/null
+++ b/includes/classes/WPCLI/Command/CLICommandInterface.php
@@ -0,0 +1,40 @@
+wp_cli = $wp_cli ?? new WP_CLI();
+ }
+
+ /**
+ * Get the name of the command
+ *
+ * @return string
+ */
+ public static function get_name(): string {
+ return 'accessibility-checker delete-stats';
+ }
+
+ /**
+ * Get the arguments for the command
+ *
+ * @return array
+ */
+ public static function get_args(): array {
+ return [
+ 'synopsis' => [
+ [
+ 'type' => 'positional',
+ 'name' => 'post_id',
+ 'description' => esc_html__( 'The ID of the post to delete stats for.', 'accessibility-checker' ),
+ 'optional' => true,
+ 'default' => 0,
+ 'repeating' => false,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Delete the accessibility-checker stats for a given post ID.
+ *
+ * @param array $options This is the positional argument, the post ID in this case.
+ * @param array $arguments This is the associative argument, not used in this command but kept for consistency with cli commands using this pattern.
+ *
+ * @return void
+ * @throws ExitException If the post ID is not provided, does not exist, or the class we need isn't available.
+ */
+ public function __invoke( array $options = [], array $arguments = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $post_id = $options[0] ?? 0;
+
+ if ( 0 === $post_id ) {
+ $this->wp_cli::error( esc_html__( 'No Post ID provided.', 'accessibility-checker' ) );
+ }
+
+ $post_exists = (bool) get_post( $post_id );
+
+ if ( ! $post_exists ) {
+ $this->wp_cli::error(
+ sprintf(
+ // translators: 1: a post ID.
+ esc_html__( 'Post ID %1$s does not exist.', 'accessibility-checker' ),
+ $post_id
+ )
+ );
+ return;
+ }
+
+ Purge_Post_Data::delete_post( $post_id );
+ $this->wp_cli::success(
+ sprintf(
+ // translators: 1: a post ID.
+ esc_html__( 'Stats of %1$s deleted.', 'accessibility-checker' ),
+ $post_id
+ )
+ );
+ }
+}
diff --git a/includes/classes/WPCLI/Command/GetSiteStats.php b/includes/classes/WPCLI/Command/GetSiteStats.php
new file mode 100644
index 00000000..6a0314b8
--- /dev/null
+++ b/includes/classes/WPCLI/Command/GetSiteStats.php
@@ -0,0 +1,128 @@
+wp_cli = $wp_cli ?? new WP_CLI();
+ }
+
+ /**
+ * Get the name of the command.
+ *
+ * @since 1.15.0
+ *
+ * @return string
+ */
+ public static function get_name(): string {
+ return 'accessibility-checker get-site-stats';
+ }
+
+ /**
+ * Get the arguments for the command
+ *
+ * @since 1.15.0
+ *
+ * @return array
+ */
+ public static function get_args(): array {
+ return [
+ 'synopsis' => [
+ [
+ 'type' => 'assoc',
+ 'name' => 'stat',
+ 'description' => esc_html__( 'Keys to show in the results. Defaults to all keys.', 'accessibility-checker' ),
+ 'optional' => true,
+ 'default' => null,
+ 'repeating' => true,
+ ],
+ [
+ 'type' => 'flag',
+ 'name' => 'clear-cache',
+ 'description' => esc_html__( 'Clear the cache before retrieving the stats (can be intensive).', 'accessibility-checker' ),
+ 'repeating' => false,
+ 'optional' => true,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Gets the accessibility-checker stats for the whole site. Use the --clear-cache flag to clear the cache before retrieving the stats.
+ *
+ * @since 1.15.0
+ *
+ * @param array $options The positional argument, none is this case.
+ * @param array $arguments The associative arguments, the stat keys in this case.
+ *
+ * @return void
+ * @throws ExitException If the post ID does not exist, or the class we need isn't available.
+ */
+ public function __invoke( array $options = [], array $arguments = [] ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ if ( ! empty( $arguments['clear-cache'] ) ) {
+ // Clear the cache.
+ ( new Scans_Stats() )->clear_cache();
+ }
+
+ $all_stats = ( new Scans_Stats() )->summary();
+
+ if ( ! empty( $arguments['stat'] ) ) {
+ $items_to_return = [];
+ $requested_stats = explode( ',', $arguments['stat'] );
+ foreach ( $requested_stats as $key ) {
+ $stats_key = trim( $key );
+ if ( ! isset( $all_stats[ $stats_key ] ) ) {
+ $this->wp_cli::error(
+ sprintf(
+ // translators: 1: a stat key that was requested but not found.
+ esc_html__( 'Stat key: %1$s not found in stats.', 'accessibility-checker' ),
+ $stats_key
+ )
+ );
+ return;
+ }
+ $items_to_return[ $stats_key ] = $all_stats[ $stats_key ];
+ }
+ }
+
+ if ( isset( $items_to_return ) ) {
+ $this->wp_cli::success( wp_json_encode( $items_to_return, JSON_PRETTY_PRINT ) );
+ return;
+ }
+
+ $this->wp_cli::success( wp_json_encode( $all_stats, JSON_PRETTY_PRINT ) );
+ }
+}
diff --git a/includes/classes/WPCLI/Command/GetStats.php b/includes/classes/WPCLI/Command/GetStats.php
new file mode 100644
index 00000000..4cd45a5f
--- /dev/null
+++ b/includes/classes/WPCLI/Command/GetStats.php
@@ -0,0 +1,177 @@
+wp_cli = $wp_cli ?? new WP_CLI();
+ }
+
+ /**
+ * Get the name of the command.
+ *
+ * @since 1.15.0
+ *
+ * @return string
+ */
+ public static function get_name(): string {
+ return 'accessibility-checker get-stats';
+ }
+
+ /**
+ * Get the arguments for the command
+ *
+ * @since 1.15.0
+ *
+ * @return array
+ */
+ public static function get_args(): array {
+ return [
+ 'synopsis' => [
+ [
+ 'type' => 'positional',
+ 'name' => 'post_id',
+ 'description' => esc_html__( 'The ID of the post to get stats for.', 'accessibility-checker' ),
+ 'optional' => true,
+ 'default' => 0,
+ 'repeating' => false,
+ ],
+ [
+ 'type' => 'assoc',
+ 'name' => 'stat',
+ 'description' => sprintf(
+ // translators: 1: a comma separated list of valid stats keys that should not be translated.
+ 'Keys to show in the results. Defaults to all keys. Pass items in as a comma separated list if you want multiple. Valid keys are: %1$s.',
+ implode( ', ', self::$valid_stats )
+ ),
+ 'optional' => true,
+ 'default' => null,
+ 'repeating' => true,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Gets the accessibility-checker stats for a given post ID.
+ *
+ * @since 1.15.0
+ *
+ * @param array $options The positional argument, the post ID in this case.
+ * @param array $arguments The associative argument, the stat key in this case.
+ *
+ * @return void
+ * @throws ExitException If the post ID does not exist, or the class we need isn't available.
+ */
+ public function __invoke( array $options = [], array $arguments = [] ) {
+ $post_id = $options[0] ?? null;
+
+ $post_exists = (bool) get_post( $post_id );
+
+ if ( ! $post_exists ) {
+ $this->wp_cli::error(
+ sprintf(
+ // translators: 1: a post ID.
+ esc_html__( 'Post ID %1$d does not exist.', 'accessibility-checker' ),
+ $post_id
+ )
+ );
+ return;
+ }
+
+ $stats = ( new Summary_Generator( $post_id ) )->generate_summary();
+
+ if (
+ empty( $stats ) ||
+ ( 100 === (int) $stats['passed_tests'] && 0 === (int) $stats['ignored'] )
+ ) {
+ $this->wp_cli::success(
+ sprintf(
+ // translators: 1: a post ID.
+ esc_html__( 'Either the post is not yet scanned or all tests passed for post ID %1$d.', 'accessibility-checker' ),
+ $post_id
+ )
+ );
+ return;
+ }
+
+ if ( ! empty( $arguments['stat'] ) ) {
+ $items_to_return = [];
+ $requested_stats = explode( ',', $arguments['stat'] );
+ foreach ( $requested_stats as $key ) {
+ $stats_key = trim( $key );
+ if ( ! in_array( $stats_key, self::$valid_stats, true ) || ! isset( $stats[ $stats_key ] ) ) {
+ $this->wp_cli::error(
+ sprintf(
+ // translators: 1: a stat key, 2: a comma separated list of valid stats keys.
+ 'Invalid stat key: %1$s. Valid keys are: %2$s.',
+ $stats_key,
+ implode( ', ', self::$valid_stats )
+ )
+ );
+
+ return;
+ }
+ $items_to_return[ $stats_key ] = $stats[ $stats_key ];
+ }
+
+ if ( $items_to_return ) {
+ $this->wp_cli::success( wp_json_encode( $items_to_return, JSON_PRETTY_PRINT ) );
+ return;
+ }
+ }
+
+ $this->wp_cli::success( wp_json_encode( $stats, JSON_PRETTY_PRINT ) );
+ }
+}
diff --git a/includes/classes/class-plugin.php b/includes/classes/class-plugin.php
index c0170b93..6bb0f558 100644
--- a/includes/classes/class-plugin.php
+++ b/includes/classes/class-plugin.php
@@ -9,6 +9,7 @@
use EDAC\Admin\Admin;
use EDAC\Admin\Meta_Boxes;
+use EqualizeDigital\AccessibilityChecker\WPCLI\BootstrapCLI;
/**
* Main plugin functionality class.
@@ -30,6 +31,12 @@ public function __construct() {
// The REST api must load if admin or not.
$rest_api = new REST_Api();
$rest_api->init_hooks();
+
+ // When WP CLI is enabled, load the CLI commands.
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ $cli = new BootstrapCLI();
+ $cli->register();
+ }
}
/**
diff --git a/phpcs.xml b/phpcs.xml
index 18e8384a..3752d709 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,6 +1,6 @@