From 0aedd0738a1eca7abf936154783d964fcfd5ea36 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 31 Mar 2026 23:03:44 +0200 Subject: [PATCH 01/10] Add initial PHPStan config --- inc/RestCommand.php | 3 ++- inc/Runner.php | 6 +++--- phpstan.neon.dist | 11 +++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/inc/RestCommand.php b/inc/RestCommand.php index d130904..6e54d10 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -2,7 +2,7 @@ namespace WP_REST_CLI; -use Spyc; +use Mustangostang\Spyc; use WP_CLI; use WP_CLI\Utils; @@ -383,6 +383,7 @@ private function do_request( $method, $route, $assoc_args ) { $request->set_param( $key, $value ); } } + $original_queries = array(); if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); } diff --git a/inc/Runner.php b/inc/Runner.php index 8597156..f134695 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -41,7 +41,7 @@ public static function load_remote_commands() { continue; } $name = $route_data['schema']['title']; - $rest_command = new RESTCommand( $name, $route, $route_data['schema'] ); + $rest_command = new RestCommand( $name, $route, $route_data['schema'] ); $rest_command->set_scope( 'http' ); $rest_command->set_api_url( $api_url ); $rest_command->set_auth( $auth ); @@ -50,7 +50,7 @@ public static function load_remote_commands() { } public static function after_wp_load() { - if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) { + if ( wp_installing() ) { return; } if ( ! class_exists( 'WP_REST_Server' ) ) { @@ -80,7 +80,7 @@ public static function after_wp_load() { continue; } $name = $route_data['schema']['title']; - $rest_command = new RESTCommand( $name, $route, $route_data['schema'] ); + $rest_command = new RestCommand( $name, $route, $route_data['schema'] ); self::register_route_commands( $rest_command, $route, $route_data ); } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..e8b6ad8 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +parameters: + level: 1 + paths: + - inc + - wp-rest-cli.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - vendor/wp-cli/mustangostang-spyc/src/Spyc.php + treatPhpDocTypesAsCertain: false From 5819f4b602da0618e27921594de8d57c9b1d8bad Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 10:46:15 +0200 Subject: [PATCH 02/10] Level 2 --- inc/RestCommand.php | 7 ++++--- inc/Runner.php | 6 ++++-- phpstan.neon.dist | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index 6e54d10..6518dff 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -554,7 +554,8 @@ private function get_filled_route( $args ) { /** * Visually depict the difference between "dictated" and "current" * - * @param array + * @param string $slug + * @param array $difference */ private function show_difference( $slug, $difference ) { $this->output_nesting_level = 0; @@ -616,7 +617,7 @@ private function recursively_show_difference( $dictated, $current = null ) { /** * Output a line to be added * - * @param string + * @param string $line */ private function add_line( $line ) { $this->nested_line( $line, 'add' ); @@ -625,7 +626,7 @@ private function add_line( $line ) { /** * Output a line to be removed * - * @param string + * @param string $line */ private function remove_line( $line ) { $this->nested_line( $line, 'remove' ); diff --git a/inc/Runner.php b/inc/Runner.php index f134695..69b0449 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -132,8 +132,10 @@ private static function get_api_index( $api_url ) { /** * Register WP-CLI commands for all endpoints on a route * - * @param string - * @param array $endpoints + * @param mixed $rest_command + * @param string $route + * @param array $route_data + * @param array $command_args */ private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e8b6ad8..1740507 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 1 + level: 2 paths: - inc - wp-rest-cli.php From 05892c41a317e2187039bdf33e733785b5bbb23f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 17:51:50 +0200 Subject: [PATCH 03/10] Level 4 --- inc/RestCommand.php | 10 +++++----- phpstan.neon.dist | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index 6518dff..70832ea 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -15,7 +15,6 @@ class RestCommand { private $route; private $resource_identifier; private $schema; - private $default_context = ''; private $output_nesting_level = 0; public function __construct( $name, $route, $schema ) { @@ -274,10 +273,11 @@ public function diff_items( $args, $assoc_args ) { } if ( ! empty( $to_item ) ) { - foreach ( array( 'to_item', 'from_item' ) as $item ) { - if ( isset( $item['_links'] ) ) { - unset( $item['_links'] ); - } + if ( isset( $to_item['_links'] ) ) { + unset( $to_item['_links'] ); + } + if ( isset( $from_item['_links'] ) ) { + unset( $from_item['_links'] ); } $display_items[] = array( 'from' => self::limit_item_to_fields( $from_item, $fields ), diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1740507..1e959dd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 2 + level: 4 paths: - inc - wp-rest-cli.php From 16fd9349e74999131a8b38f8c5bf591715ab4602 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 09:50:46 +0200 Subject: [PATCH 04/10] Level 5 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1e959dd..3c3358a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 4 + level: 5 paths: - inc - wp-rest-cli.php From a690e152822e552a3f862fa27e96cf8180a0e21a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 11:28:03 +0200 Subject: [PATCH 05/10] Level 6 --- inc/RestCommand.php | 97 +++++++++++++++++++++++++++++++++++++++------ inc/Runner.php | 24 ++++++++--- phpstan.neon.dist | 2 +- 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index 70832ea..bff57e7 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -8,15 +8,35 @@ class RestCommand { + /** @var string */ private $scope = 'internal'; + + /** @var string */ private $api_url = ''; + + /** @var array */ private $auth = array(); + + /** @var string */ private $name; + + /** @var string */ private $route; + + /** @var string */ private $resource_identifier; + + /** @var array */ private $schema; + + /** @var int */ private $output_nesting_level = 0; + /** + * @param string $name + * @param string $route + * @param array $schema + */ public function __construct( $name, $route, $schema ) { $this->name = $name; $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); @@ -29,6 +49,7 @@ public function __construct( $name, $route, $schema ) { * Set the scope of the REST requests * * @param string $scope + * @return void */ public function set_scope( $scope ) { $this->scope = $scope; @@ -38,6 +59,7 @@ public function set_scope( $scope ) { * Set the API url for the REST requests * * @param string $api_url + * @return void */ public function set_api_url( $api_url ) { $this->api_url = $api_url; @@ -46,7 +68,8 @@ public function set_api_url( $api_url ) { /** * Set the authentication for the API requests * - * @param array $auth + * @param array $auth + * @return void */ public function set_auth( $auth ) { $this->auth = $auth; @@ -56,6 +79,10 @@ public function set_auth( $auth ) { * Create a new item. * * @subcommand create + * + * @param array $args + * @param array $assoc_args + * @return void */ public function create_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); @@ -70,6 +97,10 @@ public function create_item( $args, $assoc_args ) { * Generate some items. * * @subcommand generate + * + * @param array $args + * @param array $assoc_args + * @return void */ public function generate_items( $args, $assoc_args ) { @@ -106,6 +137,10 @@ public function generate_items( $args, $assoc_args ) { * Delete an existing item. * * @subcommand delete + * + * @param array $args + * @param array $assoc_args + * @return void */ public function delete_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); @@ -123,6 +158,10 @@ public function delete_item( $args, $assoc_args ) { * Get a single item. * * @subcommand get + * + * @param array $args + * @param array $assoc_args + * @return void */ public function get_item( $args, $assoc_args ) { list( $status, $body, $headers ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); @@ -154,6 +193,10 @@ public function get_item( $args, $assoc_args ) { * List all items. * * @subcommand list + * + * @param array $args + * @param array $assoc_args + * @return void */ public function list_items( $args, $assoc_args ) { if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { @@ -208,6 +251,10 @@ public function list_items( $args, $assoc_args ) { * : Limit comparison to specific fields. * * @subcommand diff + * + * @param array $args + * @param array $assoc_args + * @return void */ public function diff_items( $args, $assoc_args ) { @@ -305,6 +352,10 @@ public function diff_items( $args, $assoc_args ) { * Update an existing item. * * @subcommand update + * + * @param array $args + * @param array $assoc_args + * @return void */ public function update_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); @@ -319,6 +370,10 @@ public function update_item( $args, $assoc_args ) { * Open an existing item in the editor * * @subcommand edit + * + * @param array $args + * @param array $assoc_args + * @return void */ public function edit_item( $args, $assoc_args ) { $assoc_args['context'] = 'edit'; @@ -366,8 +421,11 @@ public function edit_item( $args, $assoc_args ) { /** * Do a REST Request * - * @param string $method + * @param string $method + * @param string $route + * @param array $assoc_args * + * @return array{0: int, 1: mixed, 2: array} */ private function do_request( $method, $route, $assoc_args ) { if ( 'internal' === $this->scope ) { @@ -467,15 +525,16 @@ function ( $a, $b ) { return array( $response->status_code, json_decode( $response->body, true ), $response->headers->getAll() ); } WP_CLI::error( 'Invalid scope for REST command.' ); + return array( 0, '', array() ); } /** * Get Formatter object based on supplied parameters. * - * @param array $assoc_args Parameters passed to command. Determines formatting. + * @param array $assoc_args Parameters passed to command. Determines formatting. * @return \WP_CLI\Formatter */ - protected function get_formatter( &$assoc_args ) { + protected function get_formatter( $assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { if ( is_string( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); @@ -494,7 +553,7 @@ protected function get_formatter( &$assoc_args ) { * Get a list of fields present in a given context * * @param string $context - * @return array + * @return array */ private function get_context_fields( $context ) { $fields = array(); @@ -522,7 +581,7 @@ private function get_context_fields( $context ) { * @param string $object_type * * @see \WP_REST_Controller::get_additional_fields - * @return array + * @return array> */ private function get_additional_fields( $object_type ) { global $wp_rest_additional_fields; @@ -546,6 +605,9 @@ private function get_base_route() { /** * Fill the route based on provided $args + * + * @param array $args + * @return string */ private function get_filled_route( $args ) { return rtrim( $this->get_base_route(), '/' ) . '/' . $args[0]; @@ -554,8 +616,9 @@ private function get_filled_route( $args ) { /** * Visually depict the difference between "dictated" and "current" * - * @param string $slug - * @param array $difference + * @param string $slug + * @param array $difference + * @return void */ private function show_difference( $slug, $difference ) { $this->output_nesting_level = 0; @@ -566,6 +629,10 @@ private function show_difference( $slug, $difference ) { /** * Recursively output the difference between "dictated" and "current" + * + * @param mixed $dictated + * @param mixed $current + * @return void */ private function recursively_show_difference( $dictated, $current = null ) { @@ -618,6 +685,7 @@ private function recursively_show_difference( $dictated, $current = null ) { * Output a line to be added * * @param string $line + * @return void */ private function add_line( $line ) { $this->nested_line( $line, 'add' ); @@ -627,6 +695,7 @@ private function add_line( $line ) { * Output a line to be removed * * @param string $line + * @return void */ private function remove_line( $line ) { $this->nested_line( $line, 'remove' ); @@ -634,6 +703,10 @@ private function remove_line( $line ) { /** * Output a line that's appropriately nested + * + * @param string $line + * @param string|bool $change + * @return void */ private function nested_line( $line, $change = false ) { @@ -659,7 +732,7 @@ private function nested_line( $line, $change = false ) { /** * Whether or not this is an associative array * - * @param array $arr + * @param array $arr * @return bool */ private function is_assoc_array( $arr ) { @@ -678,9 +751,9 @@ private function is_assoc_array( $arr ) { /** * Reduce an item to specific fields. * - * @param array $item - * @param array $fields - * @return array + * @param array $item + * @param array|string $fields + * @return array */ private static function limit_item_to_fields( $item, $fields ) { if ( empty( $fields ) ) { diff --git a/inc/Runner.php b/inc/Runner.php index 69b0449..944623a 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -11,6 +11,8 @@ class Runner { /** * When --http=domain.com is passed as global arg, register REST for it + * + * @return void */ public static function load_remote_commands() { @@ -49,6 +51,11 @@ public static function load_remote_commands() { } } + /** + * Run after WordPress is loaded. + * + * @return void + */ public static function after_wp_load() { if ( wp_installing() ) { return; @@ -106,6 +113,12 @@ private static function auto_discover_api( $url ) { return $endpoint; } + /** + * Discover WP-API endpoint from link headers + * + * @param string $link_headers + * @return string|false + */ private static function discover_wp_api( $link_headers ) { if ( preg_match( '#<([^>]+)> *; *rel="https://api.w.org/"#', $link_headers, $matches ) ) { return $matches[1]; @@ -117,7 +130,7 @@ private static function discover_wp_api( $link_headers ) { * Get the index data from an API url * * @param string $api_url - * @return array|false + * @return array|false */ private static function get_api_index( $api_url ) { $query_char = false !== strpos( $api_url, '?' ) ? '&' : '?'; @@ -132,10 +145,11 @@ private static function get_api_index( $api_url ) { /** * Register WP-CLI commands for all endpoints on a route * - * @param mixed $rest_command - * @param string $route - * @param array $route_data - * @param array $command_args + * @param \WP_REST_CLI\RestCommand $rest_command + * @param string $route + * @param array $route_data + * @param array $command_args + * @return void */ private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3c3358a..813e025 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 6 paths: - inc - wp-rest-cli.php From 1a2ea93d8f767473125fac6a98a945c2443661ce Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 13:07:21 +0200 Subject: [PATCH 06/10] Lint fix --- inc/RestCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index bff57e7..48fb3b5 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -9,13 +9,13 @@ class RestCommand { /** @var string */ - private $scope = 'internal'; + private $scope = 'internal'; /** @var string */ private $api_url = ''; /** @var array */ - private $auth = array(); + private $auth = array(); /** @var string */ private $name; From 91c995eecdf5b781ca26aec023a960f31e467ec7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 09:41:10 +0200 Subject: [PATCH 07/10] Level 7 --- inc/RestCommand.php | 9 ++++++++- inc/Runner.php | 6 +++++- phpstan.neon.dist | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index 48fb3b5..024b58f 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -119,6 +119,7 @@ public function generate_items( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); if ( 'progress' === $format ) { + /** @var \cli\progress\Bar $notify */ $notify->tick(); } elseif ( 'ids' === $format ) { echo $body['id']; @@ -129,6 +130,7 @@ public function generate_items( $args, $assoc_args ) { } if ( 'progress' === $format ) { + /** @var \cli\progress\Bar $notify */ $notify->finish(); } } @@ -287,6 +289,9 @@ public function diff_items( $args, $assoc_args ) { $to_body = $response['body']; $to_api_url = $response['api_url']; + $from_body = is_array( $from_body ) ? $from_body : array(); + $to_body = is_array( $to_body ) ? $to_body : array(); + if ( ! is_null( $resource ) ) { $field = is_numeric( $resource ) ? 'id' : 'slug'; $callback = function ( $value ) use ( $field, $resource ) { @@ -410,7 +415,7 @@ public function edit_item( $args, $assoc_args ) { WP_CLI::error( 'Cannot edit - no editable fields found on schema.' ); } $ret = Utils\launch_editor_for_input( Spyc::YAMLDump( $editable_fields ), sprintf( 'Editing %s %s', $schema['title'], $args[0] ) ); - if ( false === $ret ) { + if ( ! is_string( $ret ) ) { WP_CLI::warning( 'No edits made.' ); } else { list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), Spyc::YAMLLoadString( $ret ) ); @@ -506,6 +511,7 @@ function ( $a, $b ) { $method = 'GET'; $assoc_args['_method'] = 'OPTIONS'; } + /** @var \WpOrg\Requests\Response $response */ $response = Utils\http_request( $method, rtrim( $this->api_url, '/' ) . $route, $assoc_args, $headers ); $body = json_decode( $response->body, true ); if ( $response->status_code >= 400 ) { @@ -522,6 +528,7 @@ function ( $a, $b ) { } } } + assert( is_int( $response->status_code ) ); return array( $response->status_code, json_decode( $response->body, true ), $response->headers->getAll() ); } WP_CLI::error( 'Invalid scope for REST command.' ); diff --git a/inc/Runner.php b/inc/Runner.php index 944623a..05af6f8 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -25,10 +25,12 @@ public static function load_remote_commands() { if ( ! $api_url ) { WP_CLI::error( "Couldn't auto-discover WP REST API endpoint from {$http}." ); } + assert( is_string( $api_url ) ); $api_index = self::get_api_index( $api_url ); if ( ! $api_index ) { WP_CLI::error( "Couldn't find index data from {$api_url}." ); } + assert( is_array( $api_index ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $bits = parse_url( $http ); $auth = array(); @@ -102,6 +104,7 @@ private static function auto_discover_api( $url ) { if ( false === stripos( $url, 'http://' ) && false === stripos( $url, 'https://' ) ) { $url = 'http://' . $url; } + /** @var \WpOrg\Requests\Response $response */ $response = Utils\http_request( 'HEAD', $url ); if ( empty( $response->headers['link'] ) ) { return false; @@ -135,7 +138,8 @@ private static function discover_wp_api( $link_headers ) { private static function get_api_index( $api_url ) { $query_char = false !== strpos( $api_url, '?' ) ? '&' : '?'; $api_url .= $query_char . 'context=help'; - $response = Utils\http_request( 'GET', $api_url ); + /** @var \WpOrg\Requests\Response $response */ + $response = Utils\http_request( 'GET', $api_url ); if ( empty( $response->body ) ) { return false; } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 813e025..5cedbb7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 paths: - inc - wp-rest-cli.php From d1918f55d74591fef44d65edb356a387f27ebb04 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 09:44:11 +0200 Subject: [PATCH 08/10] Level 8 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5cedbb7..0169002 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - inc - wp-rest-cli.php From e3804eb9b2e8347d2e1860343e2c9629e9e9db3b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 12:02:56 +0200 Subject: [PATCH 09/10] Level 9 --- inc/RestCommand.php | 256 +++++++++++++++++++++++++++++++++----------- inc/Runner.php | 81 +++++++++++--- phpstan.neon.dist | 2 +- 3 files changed, 261 insertions(+), 78 deletions(-) diff --git a/inc/RestCommand.php b/inc/RestCommand.php index 024b58f..534d852 100644 --- a/inc/RestCommand.php +++ b/inc/RestCommand.php @@ -86,8 +86,12 @@ public function set_auth( $auth ) { */ public function create_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); - if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { - WP_CLI::line( $body['id'] ); + if ( ! is_array( $body ) || empty( $body['id'] ) ) { + WP_CLI::error( "Could not create {$this->name}." ); + } + /** @var array{id: scalar} $body */ + if ( Utils\get_flag_value( self::get_typed_assoc_args( $assoc_args ), 'porcelain' ) ) { + WP_CLI::line( (string) $body['id'] ); } else { WP_CLI::success( "Created {$this->name} {$body['id']}." ); } @@ -104,9 +108,16 @@ public function create_item( $args, $assoc_args ) { */ public function generate_items( $args, $assoc_args ) { - $count = $assoc_args['count']; + $count = 0; + if ( isset( $assoc_args['count'] ) ) { + $count = is_numeric( $assoc_args['count'] ) ? (int) $assoc_args['count'] : 0; + } unset( $assoc_args['count'] ); - $format = $assoc_args['format']; + + $format = 'ids'; + if ( isset( $assoc_args['format'] ) ) { + $format = is_scalar( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'ids'; + } unset( $assoc_args['format'] ); $notify = false; @@ -122,7 +133,8 @@ public function generate_items( $args, $assoc_args ) { /** @var \cli\progress\Bar $notify */ $notify->tick(); } elseif ( 'ids' === $format ) { - echo $body['id']; + $id = is_array( $body ) && isset( $body['id'] ) ? $body['id'] : ''; + echo is_scalar( $id ) ? (string) $id : ''; if ( $i < $count - 1 ) { echo ' '; } @@ -146,11 +158,23 @@ public function generate_items( $args, $assoc_args ) { */ public function delete_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); - $id = isset( $body['previous'] ) ? $body['previous']['id'] : $body['id']; - if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + if ( ! is_array( $body ) ) { + $body = array(); + } + + $id = ''; + if ( isset( $body['previous'] ) && is_array( $body['previous'] ) && isset( $body['previous']['id'] ) ) { + $id = $body['previous']['id']; + } elseif ( isset( $body['id'] ) ) { + $id = $body['id']; + } + + $id = is_scalar( $id ) ? (string) $id : ''; + + if ( Utils\get_flag_value( self::get_typed_assoc_args( $assoc_args ), 'porcelain' ) ) { WP_CLI::line( $id ); } elseif ( empty( $assoc_args['force'] ) ) { - WP_CLI::success( "Trashed {$this->name} {$id}." ); + WP_CLI::success( "Trashed {$this->name} {$id}." ); } else { WP_CLI::success( "Deleted {$this->name} {$id}." ); } @@ -168,8 +192,21 @@ public function delete_item( $args, $assoc_args ) { public function get_item( $args, $assoc_args ) { list( $status, $body, $headers ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); + if ( ! is_array( $body ) ) { + $body = array(); + } + if ( ! empty( $assoc_args['fields'] ) ) { - $body = self::limit_item_to_fields( $body, $assoc_args['fields'] ); + $fields = $assoc_args['fields']; + if ( is_string( $fields ) ) { + $fields = explode( ',', $fields ); + } + if ( is_array( $fields ) ) { + $fields = array_filter( $fields, 'is_string' ); + } else { + $fields = array(); + } + $body = self::limit_item_to_fields( $body, $fields ); } if ( 'headers' === $assoc_args['format'] ) { @@ -207,6 +244,9 @@ public function list_items( $args, $assoc_args ) { $method = 'GET'; } list( $status, $body, $headers ) = $this->do_request( $method, $this->get_base_route(), $assoc_args ); + if ( ! is_array( $body ) ) { + $body = array(); + } if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_column( $body, 'id' ); } else { @@ -214,13 +254,26 @@ public function list_items( $args, $assoc_args ) { } if ( ! empty( $assoc_args['fields'] ) ) { + $fields = $assoc_args['fields']; + if ( is_string( $fields ) ) { + $fields = explode( ',', $fields ); + } + if ( is_array( $fields ) ) { + $fields = array_filter( $fields, 'is_string' ); + } else { + $fields = array(); + } foreach ( $items as $key => $item ) { - $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] ); + if ( is_array( $item ) ) { + /** @var array $item */ + $items[ $key ] = self::limit_item_to_fields( $item, $fields ); + } } } if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { - echo (int) $headers['X-WP-Total']; + $total = isset( $headers['X-WP-Total'] ) ? $headers['X-WP-Total'] : 0; + echo is_numeric( $total ) ? (int) $total : 0; } elseif ( 'headers' === $assoc_args['format'] ) { echo json_encode( $headers ); } elseif ( 'body' === $assoc_args['format'] ) { @@ -265,12 +318,19 @@ public function diff_items( $args, $assoc_args ) { WP_CLI::error( "Alias '{$alias}' not found." ); } $resource = isset( $args[1] ) ? $args[1] : null; - $fields = Utils\get_flag_value( $assoc_args, 'fields', null ); + $fields = Utils\get_flag_value( self::get_typed_assoc_args( $assoc_args ), 'fields', '' ); + if ( ! is_string( $fields ) ) { + $fields = ''; + } list( $from_status, $from_body, $from_headers ) = $this->do_request( 'GET', $this->get_base_route(), array() ); - $php_bin = WP_CLI::get_php_binary(); - $script_path = $GLOBALS['argv'][0]; + $php_bin = WP_CLI::get_php_binary(); + $argv = isset( $GLOBALS['argv'] ) && is_array( $GLOBALS['argv'] ) ? $GLOBALS['argv'] : array(); + $script_path = ''; + if ( isset( $argv[0] ) && is_string( $argv[0] ) ) { + $script_path = $argv[0]; + } $other_args = implode( ' ', array_map( 'escapeshellarg', array( $alias, 'rest', $this->name, 'list' ) ) ); $other_assoc_args = Utils\assoc_args_to_str( array( 'format' => 'envelope' ) ); $full_command = "{$php_bin} {$script_path} {$other_args} {$other_assoc_args}"; @@ -285,9 +345,13 @@ public function diff_items( $args, $assoc_args ) { ); $result = $process->run(); $response = json_decode( $result->stdout, true ); - $to_headers = $response['headers']; - $to_body = $response['body']; - $to_api_url = $response['api_url']; + if ( ! is_array( $response ) || ! isset( $response['headers'] ) || ! isset( $response['body'] ) || ! isset( $response['api_url'] ) || ! is_string( $response['api_url'] ) ) { + WP_CLI::error( 'Invalid response from alias.' ); + } + /** @var array{headers: mixed, body: mixed, api_url: string} $response */ + $to_headers = $response['headers']; + $to_body = $response['body']; + $to_api_url = $response['api_url']; $from_body = is_array( $from_body ) ? $from_body : array(); $to_body = is_array( $to_body ) ? $to_body : array(); @@ -311,8 +375,12 @@ public function diff_items( $args, $assoc_args ) { $to_item = array(); if ( ! empty( $from_body ) ) { $from_item = array_shift( $from_body ); + $from_item = is_array( $from_item ) ? $from_item : array(); if ( ! empty( $to_body ) && ! empty( $from_item['slug'] ) ) { foreach ( $to_body as $i => $item ) { + if ( ! is_array( $item ) ) { + continue; + } if ( ! empty( $item['slug'] ) && $item['slug'] === $from_item['slug'] ) { $to_item = $item; unset( $to_body[ $i ] ); @@ -322,6 +390,7 @@ public function diff_items( $args, $assoc_args ) { } } elseif ( ! empty( $to_body ) ) { $to_item = array_shift( $to_body ); + $to_item = is_array( $to_item ) ? $to_item : array(); } if ( ! empty( $to_item ) ) { @@ -364,8 +433,12 @@ public function diff_items( $args, $assoc_args ) { */ public function update_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); - if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { - WP_CLI::line( $body['id'] ); + if ( ! is_array( $body ) || empty( $body['id'] ) ) { + WP_CLI::error( "Could not update {$this->name}." ); + } + /** @var array{id: scalar} $body */ + if ( Utils\get_flag_value( self::get_typed_assoc_args( $assoc_args ), 'porcelain' ) ) { + WP_CLI::line( (string) $body['id'] ); } else { WP_CLI::success( "Updated {$this->name} {$body['id']}." ); } @@ -383,26 +456,38 @@ public function update_item( $args, $assoc_args ) { public function edit_item( $args, $assoc_args ) { $assoc_args['context'] = 'edit'; list( $status, $options_body ) = $this->do_request( 'OPTIONS', $this->get_filled_route( $args ), $assoc_args ); - if ( empty( $options_body['schema'] ) ) { + if ( ! is_array( $options_body ) || empty( $options_body['schema'] ) || ! is_array( $options_body['schema'] ) ) { WP_CLI::error( 'Cannot edit - no schema found for resource.' ); } - $schema = $options_body['schema']; + /** @var array{schema: array{properties: array, title: string}} $options_body */ + $schema = $options_body['schema']; + if ( empty( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) { + WP_CLI::error( 'Cannot edit - no properties found in schema.' ); + } + if ( empty( $schema['title'] ) || ! is_string( $schema['title'] ) ) { + WP_CLI::error( 'Cannot edit - no valid title in schema.' ); + } list( $status, $resource_fields ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); - $editable_fields = array(); + if ( ! is_array( $resource_fields ) ) { + WP_CLI::error( 'Cannot edit - no resource fields found.' ); + } + /** @var array $resource_fields */ + $editable_fields = array(); foreach ( $resource_fields as $key => $value ) { - if ( ! isset( $schema['properties'][ $key ] ) || ! empty( $schema['properties'][ $key ]['readonly'] ) ) { + if ( ! isset( $schema['properties'][ $key ] ) || ! is_array( $schema['properties'][ $key ] ) || ! empty( $schema['properties'][ $key ]['readonly'] ) ) { continue; } $properties = $schema['properties'][ $key ]; - if ( isset( $properties['properties'] ) ) { + if ( isset( $properties['properties'] ) && is_array( $properties['properties'] ) ) { $parent_key = $key; $properties = $properties['properties']; - foreach ( $value as $key => $value ) { - if ( isset( $properties[ $key ] ) && empty( $properties[ $key ]['readonly'] ) ) { - if ( ! isset( $editable_fields[ $parent_key ] ) ) { - $editable_fields[ $parent_key ] = array(); + if ( is_array( $value ) ) { + foreach ( $value as $sub_key => $sub_value ) { + if ( isset( $properties[ $sub_key ] ) && is_array( $properties[ $sub_key ] ) && empty( $properties[ $sub_key ]['readonly'] ) ) { + $sub_array = isset( $editable_fields[ $parent_key ] ) && is_array( $editable_fields[ $parent_key ] ) ? $editable_fields[ $parent_key ] : array(); + $sub_array[ $sub_key ] = $sub_value; + $editable_fields[ $parent_key ] = $sub_array; } - $editable_fields[ $parent_key ][ $key ] = $value; } } continue; @@ -448,12 +533,16 @@ private function do_request( $method, $route, $assoc_args ) { } $original_queries = array(); if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { - $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); + /** @var \wpdb $wpdb */ + $wpdb = $GLOBALS['wpdb']; + $original_queries = is_array( $wpdb->queries ) ? array_keys( $wpdb->queries ) : array(); } $response = rest_do_request( $request ); if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { + /** @var \wpdb $wpdb */ + $wpdb = $GLOBALS['wpdb']; $performed_queries = array(); - foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { + foreach ( (array) $wpdb->queries as $key => $query ) { if ( in_array( $key, $original_queries, true ) ) { continue; } @@ -504,8 +593,12 @@ function ( $a, $b ) { } elseif ( 'http' === $this->scope ) { $headers = array(); if ( ! empty( $this->auth ) && 'basic' === $this->auth['type'] ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode - $headers['Authorization'] = 'Basic ' . base64_encode( $this->auth['username'] . ':' . $this->auth['password'] ); + $username = isset( $this->auth['username'] ) ? $this->auth['username'] : ''; + $password = isset( $this->auth['password'] ) ? $this->auth['password'] : ''; + if ( is_scalar( $username ) && is_scalar( $password ) ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + $headers['Authorization'] = 'Basic ' . base64_encode( (string) $username . ':' . (string) $password ); + } } if ( 'OPTIONS' === $method ) { $method = 'GET'; @@ -514,8 +607,11 @@ function ( $a, $b ) { /** @var \WpOrg\Requests\Response $response */ $response = Utils\http_request( $method, rtrim( $this->api_url, '/' ) . $route, $assoc_args, $headers ); $body = json_decode( $response->body, true ); + if ( ! is_array( $body ) ) { + $body = array(); + } if ( $response->status_code >= 400 ) { - if ( ! empty( $body['message'] ) ) { + if ( ! empty( $body['message'] ) && is_string( $body['message'] ) ) { WP_CLI::error( $body['message'] . ' ' . json_encode( array( 'status' => $response->status_code ) ) ); } else { switch ( $response->status_code ) { @@ -545,11 +641,13 @@ protected function get_formatter( $assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { if ( is_string( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); + } elseif ( is_array( $assoc_args['fields'] ) ) { + $fields = array_filter( $assoc_args['fields'], 'is_string' ); } else { - $fields = $assoc_args['fields']; + $fields = array(); } - } elseif ( ! empty( $assoc_args['context'] ) ) { - $fields = $this->get_context_fields( $assoc_args['context'] ); + } elseif ( ! empty( $assoc_args['context'] ) && is_scalar( $assoc_args['context'] ) ) { + $fields = $this->get_context_fields( (string) $assoc_args['context'] ); } else { $fields = $this->get_context_fields( 'view' ); } @@ -564,18 +662,31 @@ protected function get_formatter( $assoc_args ) { */ private function get_context_fields( $context ) { $fields = array(); - foreach ( $this->schema['properties'] as $key => $args ) { - if ( empty( $args['context'] ) || in_array( $context, $args['context'], true ) ) { - $fields[] = $key; + if ( ! empty( $this->schema['properties'] ) && is_array( $this->schema['properties'] ) ) { + foreach ( $this->schema['properties'] as $key => $args ) { + if ( ! is_array( $args ) ) { + continue; + } + $context_array = isset( $args['context'] ) ? $args['context'] : array(); + if ( ! is_array( $context_array ) ) { + $context_array = array(); + } + if ( empty( $context_array ) || in_array( $context, $context_array, true ) ) { + $fields[] = (string) $key; + } } } - foreach ( $this->get_additional_fields( $this->schema['title'] ) as $field_name => $field ) { + $title = isset( $this->schema['title'] ) ? $this->schema['title'] : ''; + if ( ! is_scalar( $title ) ) { + $title = ''; + } + foreach ( $this->get_additional_fields( (string) $title ) as $field_name => $field ) { // For back-compat, include any field with an empty schema // because it won't be present in $this->get_item_schema(). // @see \WP_REST_Controller::get_fields_for_response - if ( is_null( $field['schema'] ) ) { - $fields[] = $field_name; + if ( is_array( $field ) && isset( $field['schema'] ) && is_null( $field['schema'] ) ) { + $fields[] = (string) $field_name; } } return $fields; @@ -645,42 +756,49 @@ private function recursively_show_difference( $dictated, $current = null ) { ++$this->output_nesting_level; - if ( $this->is_assoc_array( $dictated ) ) { + if ( is_array( $dictated ) && $this->is_assoc_array( $dictated ) ) { foreach ( $dictated as $key => $value ) { + $key_str = (string) $key; + + if ( is_array( $value ) ) { - if ( $this->is_assoc_array( $value ) || is_array( $value ) ) { + $new_current = null; + if ( is_array( $current ) && isset( $current[ $key ] ) ) { + $new_current = $current[ $key ]; + } - $new_current = isset( $current[ $key ] ) ? $current[ $key ] : null; if ( $new_current ) { - $this->nested_line( $key . ': ' ); + $this->nested_line( $key_str . ': ' ); } else { - $this->add_line( $key . ': ' ); + $this->add_line( $key_str . ': ' ); } $this->recursively_show_difference( $value, $new_current ); - } elseif ( is_string( $value ) ) { - - $pre = $key . ': '; - - if ( isset( $current[ $key ] ) && $current[ $key ] !== $value ) { + } elseif ( is_scalar( $value ) ) { - $this->remove_line( $pre . $current[ $key ] ); - $this->add_line( $pre . $value ); - - } elseif ( ! isset( $current[ $key ] ) ) { - - $this->add_line( $pre . $value ); + $pre = $key_str . ': '; + $value_str = (string) $value; + if ( is_array( $current ) && isset( $current[ $key ] ) ) { + $current_val = $current[ $key ]; + if ( $current_val !== $value ) { + $current_val_str = is_scalar( $current_val ) ? (string) $current_val : ''; + $this->remove_line( $pre . $current_val_str ); + $this->add_line( $pre . $value_str ); + } + } else { + $this->add_line( $pre . $value_str ); } } } } elseif ( is_array( $dictated ) ) { foreach ( $dictated as $value ) { - if ( ! $current || ! in_array( $value, $current, true ) ) { - $this->add_line( '- ' . $value ); + $value_str = is_scalar( $value ) ? (string) $value : ''; + if ( ! is_array( $current ) || ! in_array( $value, $current, true ) ) { + $this->add_line( '- ' . $value_str ); } } } @@ -776,4 +894,20 @@ private static function limit_item_to_fields( $item, $fields ) { } return $item; } + + /** + * Get typed assoc args for WP-CLI utilities. + * + * @param array $assoc_args + * @return array + */ + private static function get_typed_assoc_args( array $assoc_args ) { + $typed = array(); + foreach ( $assoc_args as $key => $value ) { + if ( is_string( $key ) && ( is_string( $value ) || is_bool( $value ) ) ) { + $typed[ $key ] = $value; + } + } + return $typed; + } } diff --git a/inc/Runner.php b/inc/Runner.php index 05af6f8..a7688c4 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -39,17 +39,30 @@ public static function load_remote_commands() { $auth['username'] = $bits['user']; $auth['password'] = ! empty( $bits['pass'] ) ? $bits['pass'] : ''; } - foreach ( $api_index['routes'] as $route => $route_data ) { - if ( empty( $route_data['schema']['title'] ) ) { - WP_CLI::debug( "No schema title found for {$route}, skipping REST command registration.", 'rest' ); + if ( ! isset( $api_index['routes'] ) || ! is_array( $api_index['routes'] ) ) { + WP_CLI::error( "No routes found in API index from {$api_url}." ); + } + /** @var array> $routes */ + $routes = $api_index['routes']; + foreach ( $routes as $route => $route_data ) { + if ( ! is_array( $route_data ) ) { + continue; + } + if ( empty( $route_data['schema'] ) || ! is_array( $route_data['schema'] ) ) { + continue; + } + if ( empty( $route_data['schema']['title'] ) || ! is_string( $route_data['schema']['title'] ) ) { + WP_CLI::debug( "No valid schema title found for {$route}, skipping REST command registration.", 'rest' ); continue; } $name = $route_data['schema']['title']; - $rest_command = new RestCommand( $name, $route, $route_data['schema'] ); + /** @var array $schema */ + $schema = $route_data['schema']; + $rest_command = new RestCommand( $name, $route, $schema ); $rest_command->set_scope( 'http' ); $rest_command->set_api_url( $api_url ); $rest_command->set_auth( $auth ); - self::register_route_commands( $rest_command, $route, $route_data, array( 'when' => 'before_wp_load' ) ); + self::register_route_commands( $rest_command, (string) $route, $route_data, array( 'when' => 'before_wp_load' ) ); } } @@ -83,14 +96,27 @@ public static function after_wp_load() { return; } - foreach ( $response_data['routes'] as $route => $route_data ) { - if ( empty( $route_data['schema']['title'] ) ) { - WP_CLI::debug( "No schema title found for {$route}, skipping REST command registration.", 'rest' ); + if ( ! is_array( $response_data ) || ! isset( $response_data['routes'] ) || ! is_array( $response_data['routes'] ) ) { + return; + } + /** @var array> $routes */ + $routes = $response_data['routes']; + foreach ( $routes as $route => $route_data ) { + if ( ! is_array( $route_data ) ) { + continue; + } + if ( empty( $route_data['schema'] ) || ! is_array( $route_data['schema'] ) ) { + continue; + } + if ( empty( $route_data['schema']['title'] ) || ! is_string( $route_data['schema']['title'] ) ) { + WP_CLI::debug( "No valid schema title found for {$route}, skipping REST command registration.", 'rest' ); continue; } $name = $route_data['schema']['title']; - $rest_command = new RestCommand( $name, $route, $route_data['schema'] ); - self::register_route_commands( $rest_command, $route, $route_data ); + /** @var array $schema */ + $schema = $route_data['schema']; + $rest_command = new RestCommand( $name, $route, $schema ); + self::register_route_commands( $rest_command, (string) $route, $route_data ); } } @@ -143,7 +169,9 @@ private static function get_api_index( $api_url ) { if ( empty( $response->body ) ) { return false; } - return json_decode( $response->body, true ); + $index = json_decode( $response->body, true ); + /** @var array|false $index */ + return $index; } /** @@ -157,9 +185,19 @@ private static function get_api_index( $api_url ) { */ private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) { + if ( empty( $route_data['schema'] ) || ! is_array( $route_data['schema'] ) ) { + return; + } + if ( empty( $route_data['schema']['title'] ) || ! is_string( $route_data['schema']['title'] ) ) { + return; + } + $parent = "rest {$route_data['schema']['title']}"; $supported_commands = array(); + if ( empty( $route_data['endpoints'] ) || ! is_array( $route_data['endpoints'] ) ) { + return; + } foreach ( $route_data['endpoints'] as $endpoint ) { $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); @@ -167,35 +205,43 @@ private static function register_route_commands( $rest_command, $route, $route_d $trimmed_route = rtrim( $route ); $is_singular = $resource_id && substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id; + if ( ! is_array( $endpoint ) ) { + continue; + } + if ( empty( $endpoint['methods'] ) || ! is_array( $endpoint['methods'] ) ) { + continue; + } + $command = ''; // List a collection if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) { - $supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + $supported_commands['list'] = ( isset( $endpoint['args'] ) && is_array( $endpoint['args'] ) ) ? $endpoint['args'] : array(); } // Create a specific resource if ( array( 'POST' ) === $endpoint['methods'] && ! $is_singular ) { - $supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + $supported_commands['create'] = ( isset( $endpoint['args'] ) && is_array( $endpoint['args'] ) ) ? $endpoint['args'] : array(); } // Get a specific resource if ( array( 'GET' ) === $endpoint['methods'] && $is_singular ) { - $supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + $supported_commands['get'] = ( isset( $endpoint['args'] ) && is_array( $endpoint['args'] ) ) ? $endpoint['args'] : array(); } // Update a specific resource if ( in_array( 'POST', $endpoint['methods'], true ) && $is_singular ) { - $supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + $supported_commands['update'] = ( isset( $endpoint['args'] ) && is_array( $endpoint['args'] ) ) ? $endpoint['args'] : array(); } // Delete a specific resource if ( array( 'DELETE' ) === $endpoint['methods'] && $is_singular ) { - $supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array(); + $supported_commands['delete'] = ( isset( $endpoint['args'] ) && is_array( $endpoint['args'] ) ) ? $endpoint['args'] : array(); } } + /** @var array> $supported_commands */ foreach ( $supported_commands as $command => $endpoint_args ) { $synopsis = array(); @@ -209,6 +255,9 @@ private static function register_route_commands( $rest_command, $route, $route_d } foreach ( $endpoint_args as $name => $args ) { + if ( ! is_array( $args ) ) { + continue; + } $arg_reg = array( 'name' => $name, 'type' => 'assoc', diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0169002..8168aa3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 8 + level: 9 paths: - inc - wp-rest-cli.php From 5c9419fd54f5294a37cc3b4315d84b24e4bd57b4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 12:32:36 +0200 Subject: [PATCH 10/10] Lint fix --- inc/Runner.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/Runner.php b/inc/Runner.php index a7688c4..8e96485 100644 --- a/inc/Runner.php +++ b/inc/Runner.php @@ -55,7 +55,7 @@ public static function load_remote_commands() { WP_CLI::debug( "No valid schema title found for {$route}, skipping REST command registration.", 'rest' ); continue; } - $name = $route_data['schema']['title']; + $name = $route_data['schema']['title']; /** @var array $schema */ $schema = $route_data['schema']; $rest_command = new RestCommand( $name, $route, $schema ); @@ -112,7 +112,7 @@ public static function after_wp_load() { WP_CLI::debug( "No valid schema title found for {$route}, skipping REST command registration.", 'rest' ); continue; } - $name = $route_data['schema']['title']; + $name = $route_data['schema']['title']; /** @var array $schema */ $schema = $route_data['schema']; $rest_command = new RestCommand( $name, $route, $schema );