class-wc-cli-runner.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <?php
  2. /**
  3. * WP_CLI_Runner class file.
  4. *
  5. * @package WooCommerce\CLI
  6. */
  7. if ( ! defined( 'ABSPATH' ) ) {
  8. exit;
  9. }
  10. /**
  11. * WC API to WC CLI Bridge.
  12. *
  13. * Hooks into the REST API, figures out which endpoints come from WC,
  14. * and registers them as CLI commands.
  15. *
  16. * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
  17. * https://github.com/wp-cli/restful
  18. *
  19. * @version 3.0.0
  20. * @package WooCommerce
  21. */
  22. class WC_CLI_Runner {
  23. /**
  24. * Endpoints to disable (meaning they will not be available as CLI commands).
  25. * Some of these can either be done via WP already, or are offered with
  26. * some other changes (like tools).
  27. *
  28. * @var array
  29. */
  30. private static $disabled_endpoints = array(
  31. 'settings',
  32. 'settings/(?P<group_id>[\w-]+)',
  33. 'settings/(?P<group_id>[\w-]+)/batch',
  34. 'settings/(?P<group_id>[\w-]+)/(?P<id>[\w-]+)',
  35. 'system_status',
  36. 'system_status/tools',
  37. 'system_status/tools/(?P<id>[\w-]+)',
  38. 'reports',
  39. 'reports/sales',
  40. 'reports/top_sellers',
  41. );
  42. /**
  43. * The version of the REST API we should target to
  44. * generate commands.
  45. *
  46. * @var string
  47. */
  48. private static $target_rest_version = 'v2';
  49. /**
  50. * Register's all endpoints as commands once WP and WC have all loaded.
  51. */
  52. public static function after_wp_load() {
  53. global $wp_rest_server;
  54. $wp_rest_server = new WP_REST_Server();
  55. do_action( 'rest_api_init', $wp_rest_server );
  56. $request = new WP_REST_Request( 'GET', '/' );
  57. $request->set_param( 'context', 'help' );
  58. $response = $wp_rest_server->dispatch( $request );
  59. $response_data = $response->get_data();
  60. if ( empty( $response_data ) ) {
  61. return;
  62. }
  63. // Loop through all of our endpoints and register any valid WC endpoints.
  64. foreach ( $response_data['routes'] as $route => $route_data ) {
  65. // Only register endpoints for WC and our target version.
  66. if ( substr( $route, 0, 4 + strlen( self::$target_rest_version ) ) !== '/wc/' . self::$target_rest_version ) {
  67. continue;
  68. }
  69. // Only register endpoints with schemas.
  70. if ( empty( $route_data['schema']['title'] ) ) {
  71. /* translators: %s: Route to a given WC-API endpoint */
  72. WP_CLI::debug( sprintf( __( 'No schema title found for %s, skipping REST command registration.', 'woocommerce' ), $route ), 'wc' );
  73. continue;
  74. }
  75. // Ignore batch endpoints.
  76. if ( 'batch' === $route_data['schema']['title'] ) {
  77. continue;
  78. }
  79. // Disable specific endpoints.
  80. $route_pieces = explode( '/', $route );
  81. $endpoint_piece = str_replace( '/wc/' . $route_pieces[2] . '/', '', $route );
  82. if ( in_array( $endpoint_piece, self::$disabled_endpoints, true ) ) {
  83. continue;
  84. }
  85. self::register_route_commands( new WC_CLI_REST_Command( $route_data['schema']['title'], $route, $route_data['schema'] ), $route, $route_data );
  86. }
  87. }
  88. /**
  89. * Generates command information and tells WP CLI about all
  90. * commands available from a route.
  91. *
  92. * @param string $rest_command WC-API command.
  93. * @param string $route Path to route endpoint.
  94. * @param array $route_data Command data.
  95. * @param array $command_args WP-CLI command arguments.
  96. */
  97. private static function register_route_commands( $rest_command, $route, $route_data, $command_args = array() ) {
  98. // Define IDs that we are looking for in the routes (in addition to id)
  99. // so that we can pass it to the rest command, and use it here to generate documentation.
  100. $supported_ids = array(
  101. 'product_id' => __( 'Product ID.', 'woocommerce' ),
  102. 'customer_id' => __( 'Customer ID.', 'woocommerce' ),
  103. 'order_id' => __( 'Order ID.', 'woocommerce' ),
  104. 'refund_id' => __( 'Refund ID.', 'woocommerce' ),
  105. 'attribute_id' => __( 'Attribute ID.', 'woocommerce' ),
  106. 'zone_id' => __( 'Zone ID.', 'woocommerce' ),
  107. 'id' => __( 'ID.', 'woocommerce' ),
  108. );
  109. $rest_command->set_supported_ids( $supported_ids );
  110. $positional_args = array_keys( $supported_ids );
  111. $parent = "wc {$route_data['schema']['title']}";
  112. $supported_commands = array();
  113. // Get a list of supported commands for each route.
  114. foreach ( $route_data['endpoints'] as $endpoint ) {
  115. preg_match_all( '#\([^\)]+\)#', $route, $matches );
  116. $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
  117. $trimmed_route = rtrim( $route );
  118. $is_singular = substr( $trimmed_route, - strlen( $resource_id ) ) === $resource_id;
  119. // List a collection.
  120. if ( array( 'GET' ) === $endpoint['methods'] && ! $is_singular ) {
  121. $supported_commands['list'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
  122. }
  123. // Create a specific resource.
  124. if ( array( 'POST' ) === $endpoint['methods'] && ! $is_singular ) {
  125. $supported_commands['create'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
  126. }
  127. // Get a specific resource.
  128. if ( array( 'GET' ) === $endpoint['methods'] && $is_singular ) {
  129. $supported_commands['get'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
  130. }
  131. // Update a specific resource.
  132. if ( in_array( 'POST', $endpoint['methods'], true ) && $is_singular ) {
  133. $supported_commands['update'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
  134. }
  135. // Delete a specific resource.
  136. if ( array( 'DELETE' ) === $endpoint['methods'] && $is_singular ) {
  137. $supported_commands['delete'] = ! empty( $endpoint['args'] ) ? $endpoint['args'] : array();
  138. }
  139. }
  140. foreach ( $supported_commands as $command => $endpoint_args ) {
  141. $synopsis = array();
  142. $arg_regs = array();
  143. $ids = array();
  144. foreach ( $supported_ids as $id_name => $id_desc ) {
  145. if ( strpos( $route, '<' . $id_name . '>' ) !== false ) {
  146. $synopsis[] = array(
  147. 'name' => $id_name,
  148. 'type' => 'positional',
  149. 'description' => $id_desc,
  150. 'optional' => false,
  151. );
  152. $ids[] = $id_name;
  153. }
  154. }
  155. if ( in_array( $command, array( 'delete', 'get', 'update' ), true ) && ! in_array( 'id', $ids, true ) ) {
  156. $synopsis[] = array(
  157. 'name' => 'id',
  158. 'type' => 'positional',
  159. 'description' => __( 'The id for the resource.', 'woocommerce' ),
  160. 'optional' => false,
  161. );
  162. }
  163. foreach ( $endpoint_args as $name => $args ) {
  164. if ( ! in_array( $name, $positional_args, true ) || strpos( $route, '<' . $id_name . '>' ) === false ) {
  165. $arg_regs[] = array(
  166. 'name' => $name,
  167. 'type' => 'assoc',
  168. 'description' => ! empty( $args['description'] ) ? $args['description'] : '',
  169. 'optional' => empty( $args['required'] ),
  170. );
  171. }
  172. }
  173. foreach ( $arg_regs as $arg_reg ) {
  174. $synopsis[] = $arg_reg;
  175. }
  176. if ( in_array( $command, array( 'list', 'get' ), true ) ) {
  177. $synopsis[] = array(
  178. 'name' => 'fields',
  179. 'type' => 'assoc',
  180. 'description' => __( 'Limit response to specific fields. Defaults to all fields.', 'woocommerce' ),
  181. 'optional' => true,
  182. );
  183. $synopsis[] = array(
  184. 'name' => 'field',
  185. 'type' => 'assoc',
  186. 'description' => __( 'Get the value of an individual field.', 'woocommerce' ),
  187. 'optional' => true,
  188. );
  189. $synopsis[] = array(
  190. 'name' => 'format',
  191. 'type' => 'assoc',
  192. 'description' => __( 'Render response in a particular format.', 'woocommerce' ),
  193. 'optional' => true,
  194. 'default' => 'table',
  195. 'options' => array(
  196. 'table',
  197. 'json',
  198. 'csv',
  199. 'ids',
  200. 'yaml',
  201. 'count',
  202. 'headers',
  203. 'body',
  204. 'envelope',
  205. ),
  206. );
  207. }
  208. if ( in_array( $command, array( 'create', 'update', 'delete' ), true ) ) {
  209. $synopsis[] = array(
  210. 'name' => 'porcelain',
  211. 'type' => 'flag',
  212. 'description' => __( 'Output just the id when the operation is successful.', 'woocommerce' ),
  213. 'optional' => true,
  214. );
  215. }
  216. $methods = array(
  217. 'list' => 'list_items',
  218. 'create' => 'create_item',
  219. 'delete' => 'delete_item',
  220. 'get' => 'get_item',
  221. 'update' => 'update_item',
  222. );
  223. $before_invoke = null;
  224. if ( empty( $command_args['when'] ) && \WP_CLI::get_config( 'debug' ) ) {
  225. $before_invoke = function() {
  226. wc_maybe_define_constant( 'SAVEQUERIES', true );
  227. };
  228. }
  229. WP_CLI::add_command(
  230. "{$parent} {$command}",
  231. array( $rest_command, $methods[ $command ] ),
  232. array(
  233. 'synopsis' => $synopsis,
  234. 'when' => ! empty( $command_args['when'] ) ? $command_args['when'] : '',
  235. 'before_invoke' => $before_invoke,
  236. )
  237. );
  238. }
  239. }
  240. }