WC_CLI_REST_Command

Main Command for WooCommere CLI.

Defined (1)

The class is defined in the following location(s).

/includes/cli/class-wc-cli-rest-command.php  
  1. class WC_CLI_REST_Command { 
  2. /** 
  3. * Endpoints that have a parent ID. 
  4. * Ex: Product reviews, which has a product ID and a review ID. 
  5. */ 
  6. protected $routes_with_parent_id = array( 
  7. 'customer_download',  
  8. 'product_review',  
  9. 'order_note',  
  10. 'shop_order_refund',  
  11. ); 
  12.  
  13. /** 
  14. * Name of command/endpoint object. 
  15. */ 
  16. private $name; 
  17.  
  18. /** 
  19. * Endpoint route. 
  20. */ 
  21. private $route; 
  22.  
  23. /** 
  24. * Main resource ID. 
  25. */ 
  26. private $resource_identifier; 
  27.  
  28. /** 
  29. * Schema for command. 
  30. */ 
  31. private $schema; 
  32.  
  33. /** 
  34. * Nesting level. 
  35. */ 
  36. private $output_nesting_level = 0; 
  37.  
  38. /** 
  39. * List of supported IDs and their description (name => desc). 
  40. */ 
  41. private $supported_ids = array(); 
  42.  
  43. /** 
  44. * Sets up REST Command. 
  45. * @param string $name Name of endpoint object (comes from schema) 
  46. * @param string $route Path to route of this endpoint 
  47. * @param array $schema Schema object 
  48. */ 
  49. public function __construct( $name, $route, $schema ) { 
  50. $this->name = $name; 
  51. $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); 
  52. $first_match = $matches[0]; 
  53. $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; 
  54. $this->route = rtrim( $route ); 
  55. $this->schema = $schema; 
  56.  
  57. $this->resource_identifier = $resource_id; 
  58. if ( in_array( $name, $this->routes_with_parent_id ) ) { 
  59. $is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id; 
  60. if ( ! $is_singular ) { 
  61. $this->resource_identifier = $first_match[0]; 
  62.  
  63. /** 
  64. * Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. 
  65. * @param array $supported_ids 
  66. */ 
  67. public function set_supported_ids( $supported_ids = array() ) { 
  68. $this->supported_ids = $supported_ids; 
  69.  
  70. /** 
  71. * Peturns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id. 
  72. * @return array 
  73. */ 
  74. public function get_supported_ids() { 
  75. return $this->supported_ids; 
  76.  
  77. /** 
  78. * Create a new item. 
  79. * @subcommand create 
  80. */ 
  81. public function create_item( $args, $assoc_args ) { 
  82. $assoc_args = self::decode_json( $assoc_args ); 
  83. list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); 
  84. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { 
  85. WP_CLI::line( $body['id'] ); 
  86. } else { 
  87. WP_CLI::success( "Created {$this->name} {$body['id']}." ); 
  88.  
  89. /** 
  90. * Delete an existing item. 
  91. * @subcommand delete 
  92. */ 
  93. public function delete_item( $args, $assoc_args ) { 
  94. list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); 
  95. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { 
  96. WP_CLI::line( $body['id'] ); 
  97. } else { 
  98. if ( empty( $assoc_args['force'] ) ) { 
  99. WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$body['id']}" ); 
  100. } else { 
  101. WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$body['id']}." ); 
  102.  
  103. /** 
  104. * Get a single item. 
  105. * @subcommand get 
  106. */ 
  107. public function get_item( $args, $assoc_args ) { 
  108. $route = $this->get_filled_route( $args ); 
  109. list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args ); 
  110.  
  111. if ( ! empty( $assoc_args['fields'] ) ) { 
  112. $body = self::limit_item_to_fields( $body, $assoc_args['fields'] ); 
  113.  
  114. if ( 'headers' === $assoc_args['format'] ) { 
  115. echo json_encode( $headers ); 
  116. } elseif ( 'body' === $assoc_args['format'] ) { 
  117. echo json_encode( $body ); 
  118. } elseif ( 'envelope' === $assoc_args['format'] ) { 
  119. echo json_encode( array( 
  120. 'body' => $body,  
  121. 'headers' => $headers,  
  122. 'status' => $status,  
  123. ) ); 
  124. } else { 
  125. $formatter = $this->get_formatter( $assoc_args ); 
  126. $formatter->display_item( $body ); 
  127.  
  128. /** 
  129. * List all items. 
  130. * @subcommand list 
  131. */ 
  132. public function list_items( $args, $assoc_args ) { 
  133. if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { 
  134. $method = 'HEAD'; 
  135. } else { 
  136. $method = 'GET'; 
  137.  
  138. list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args ); 
  139. if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { 
  140. $items = array_column( $body, 'id' ); 
  141. } else { 
  142. $items = $body; 
  143.  
  144. if ( ! empty( $assoc_args['fields'] ) ) { 
  145. foreach ( $items as $key => $item ) { 
  146. $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] ); 
  147.  
  148. if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { 
  149. echo (int) $headers['X-WP-Total']; 
  150. } elseif ( 'headers' === $assoc_args['format'] ) { 
  151. echo json_encode( $headers ); 
  152. } elseif ( 'body' === $assoc_args['format'] ) { 
  153. echo json_encode( $body ); 
  154. } elseif ( 'envelope' === $assoc_args['format'] ) { 
  155. echo json_encode( array( 
  156. 'body' => $body,  
  157. 'headers' => $headers,  
  158. 'status' => $status,  
  159. 'api_url' => $this->api_url,  
  160. ) ); 
  161. } else { 
  162. $formatter = $this->get_formatter( $assoc_args ); 
  163. $formatter->display_items( $items ); 
  164.  
  165. /** 
  166. * Update an existing item. 
  167. * @subcommand update 
  168. */ 
  169. public function update_item( $args, $assoc_args ) { 
  170. $assoc_args = self::decode_json( $assoc_args ); 
  171. list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); 
  172. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { 
  173. WP_CLI::line( $body['id'] ); 
  174. } else { 
  175. WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." ); 
  176.  
  177. /** 
  178. * Do a REST Request 
  179. * @param string $method 
  180. */ 
  181. private function do_request( $method, $route, $assoc_args ) { 
  182. if ( ! defined( 'REST_REQUEST' ) ) { 
  183. define( 'REST_REQUEST', true ); 
  184. $request = new WP_REST_Request( $method, $route ); 
  185. if ( in_array( $method, array( 'POST', 'PUT' ) ) ) { 
  186. $request->set_body_params( $assoc_args ); 
  187. } else { 
  188. foreach ( $assoc_args as $key => $value ) { 
  189. $request->set_param( $key, $value ); 
  190. if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { 
  191. $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); 
  192. $response = rest_do_request( $request ); 
  193. if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { 
  194. $performed_queries = array(); 
  195. foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { 
  196. if ( in_array( $key, $original_queries ) ) { 
  197. continue; 
  198. $performed_queries[] = $query; 
  199. usort( $performed_queries, function( $a, $b ) { 
  200. if ( $a[1] === $b[1] ) { 
  201. return 0; 
  202. return ( $a[1] > $b[1] ) ? -1 : 1; 
  203. }); 
  204.  
  205. $query_count = count( $performed_queries ); 
  206. $query_total_time = 0; 
  207. foreach ( $performed_queries as $query ) { 
  208. $query_total_time += $query[1]; 
  209. $slow_query_message = ''; 
  210. if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) { 
  211. $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; 
  212. foreach ( $performed_queries as $i => $query ) { 
  213. $i++; 
  214. $bits = explode( ', ', $query[2] ); 
  215. $backtrace = implode( ', ', array_slice( $bits, 13 ) ); 
  216. $seconds = round( $query[1], 6 ); 
  217. $slow_query_message .= <<<EOT 
  218. {$i}: 
  219. - {$seconds} seconds 
  220. - {$backtrace} 
  221. - {$query[0]} 
  222. EOT; 
  223. $slow_query_message .= PHP_EOL; 
  224. } elseif ( 'wc' !== WP_CLI::get_config( 'debug' ) ) { 
  225. $slow_query_message = '. Use --debug=wc to see all queries.'; 
  226. $query_total_time = round( $query_total_time, 6 ); 
  227. WP_CLI::debug( "wc command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'wc' ); 
  228.  
  229. if ( $error = $response->as_error() ) { 
  230. // For authentication errors (status 401), include a reminder to set the --user flag. 
  231. // WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead. 
  232. if ( 401 === $response->get_status() ) { 
  233. $errors = $error->get_error_messages(); 
  234. $errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}'; 
  235. $error = implode( "\n", $errors ); 
  236. WP_CLI::error( $error ); 
  237. return array( $response->get_status(), $response->get_data(), $response->get_headers() ); 
  238.  
  239. /** 
  240. * Get Formatter object based on supplied parameters. 
  241. * @param array $assoc_args Parameters passed to command. Determines formatting. 
  242. * @return \WP_CLI\Formatter 
  243. */ 
  244. protected function get_formatter( &$assoc_args ) { 
  245. if ( ! empty( $assoc_args['fields'] ) ) { 
  246. if ( is_string( $assoc_args['fields'] ) ) { 
  247. $fields = explode( ', ', $assoc_args['fields'] ); 
  248. } else { 
  249. $fields = $assoc_args['fields']; 
  250. } else { 
  251. if ( ! empty( $assoc_args['context'] ) ) { 
  252. $fields = $this->get_context_fields( $assoc_args['context'] ); 
  253. } else { 
  254. $fields = $this->get_context_fields( 'view' ); 
  255. return new \WP_CLI\Formatter( $assoc_args, $fields ); 
  256.  
  257. /** 
  258. * Get a list of fields present in a given context 
  259. * @param string $context 
  260. * @return array 
  261. */ 
  262. private function get_context_fields( $context ) { 
  263. $fields = array(); 
  264. foreach ( $this->schema['properties'] as $key => $args ) { 
  265. if ( empty( $args['context'] ) || in_array( $context, $args['context'] ) ) { 
  266. $fields[] = $key; 
  267. return $fields; 
  268.  
  269. /** 
  270. * Get the route for this resource 
  271. * @param array $args 
  272. * @return string 
  273. */ 
  274. private function get_filled_route( $args = array() ) { 
  275. $supported_id_matched = false; 
  276. $route = $this->route; 
  277.  
  278. foreach ( $this->get_supported_ids() as $id_name => $id_desc ) { 
  279. if ( strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) { 
  280. $route = str_replace( '(?P<' . $id_name . '>[\d]+)', $args[0], $route ); 
  281. $supported_id_matched = true; 
  282.  
  283. if ( ! empty( $args ) ) { 
  284. $id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0]; 
  285. $route = str_replace( array( '(?P<id>[\d]+)', '(?P<id>[\w-]+)' ), $id_replacement, $route ); 
  286.  
  287. return rtrim( $route ); 
  288.  
  289. /** 
  290. * Output a line to be added 
  291. * @param string 
  292. */ 
  293. private function add_line( $line ) { 
  294. $this->nested_line( $line, 'add' ); 
  295.  
  296. /** 
  297. * Output a line to be removed 
  298. * @param string 
  299. */ 
  300. private function remove_line( $line ) { 
  301. $this->nested_line( $line, 'remove' ); 
  302.  
  303. /** 
  304. * Output a line that's appropriately nested 
  305. */ 
  306. private function nested_line( $line, $change = false ) { 
  307. if ( 'add' == $change ) { 
  308. $label = '+ '; 
  309. } elseif ( 'remove' == $change ) { 
  310. $label = '- '; 
  311. } else { 
  312. $label = false; 
  313.  
  314. $spaces = ( $this->output_nesting_level * 2 ) + 2; 
  315. if ( $label ) { 
  316. $line = $label . $line; 
  317. $spaces = $spaces - 2; 
  318. WP_CLI::line( str_pad( ' ', $spaces ) . $line ); 
  319.  
  320. /** 
  321. * Whether or not this is an associative array 
  322. * @param array 
  323. * @return bool 
  324. */ 
  325. private function is_assoc_array( $array ) { 
  326. if ( ! is_array( $array ) ) { 
  327. return false; 
  328. return array_keys( $array ) !== range( 0, count( $array ) - 1 ); 
  329.  
  330. /** 
  331. * Reduce an item to specific fields. 
  332. * @param array $item 
  333. * @param array $fields 
  334. * @return array 
  335. */ 
  336. private static function limit_item_to_fields( $item, $fields ) { 
  337. if ( empty( $fields ) ) { 
  338. return $item; 
  339. if ( is_string( $fields ) ) { 
  340. $fields = explode( ', ', $fields ); 
  341. foreach ( $item as $i => $field ) { 
  342. if ( ! in_array( $i, $fields ) ) { 
  343. unset( $item[ $i ] ); 
  344. return $item; 
  345.  
  346. /** 
  347. * JSON can be passed in some more complicated objects, like the payment gateway settings array. 
  348. * This function decodes the json (if present) and tries to get it's value. 
  349. * @param array $arr 
  350. */ 
  351. protected function decode_json( $arr ) { 
  352. foreach ( $arr as $key => $value ) { 
  353. if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) { 
  354. $arr[ $key ] = json_decode( $value, true ); 
  355. } else { 
  356. continue; 
  357. return $arr; 
  358.