Command
LifterLMS CLI Restful Commands
Contents
Source Source
File: libraries/lifterlms-cli/src/Commands/Restful/Command.php
class Command { private $scope = 'internal'; private $api_url = ''; private $auth = array(); private $name; private $route; private $resource_identifier; private $schema; private $default_context = ''; private $output_nesting_level = 0; public function __construct( $name, $route, $schema ) { $this->name = $name; $parsed_args = preg_match_all( '#\([^\)]+\)#', $route, $matches ); $this->resource_identifier = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null; $this->route = rtrim( $route ); $this->schema = $schema; } /** * Create a new item. * * @subcommand create */ public function create_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { \WP_CLI::line( $body['id'] ); } else { \WP_CLI::success( "Created {$this->name} {$body['id']}." ); } } /** * Generate some items. * * @subcommand generate */ public function generate_items( $args, $assoc_args ) { $count = $assoc_args['count']; unset( $assoc_args['count'] ); $format = $assoc_args['format']; unset( $assoc_args['format'] ); $notify = false; if ( 'progress' === $format ) { $notify = \WP_CLI\Utils\make_progress_bar( 'Generating items', $count ); } for ( $i = 0; $i < $count; $i++ ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_base_route(), $assoc_args ); if ( 'progress' === $format ) { $notify->tick(); } elseif ( 'ids' === $format ) { echo $body['id']; if ( $i < $count - 1 ) { echo ' '; } } } if ( 'progress' === $format ) { $notify->finish(); } } /** * Delete an existing item. * * @subcommand delete */ 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 ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { \WP_CLI::line( $id ); } else { if ( empty( $assoc_args['force'] ) ) { \WP_CLI::success( "Trashed {$this->name} {$id}." ); } else { \WP_CLI::success( "Deleted {$this->name} {$id}." ); } } } /** * Get a single item. * * @subcommand get */ public function get_item( $args, $assoc_args ) { list( $status, $body, $headers ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); if ( ! empty( $assoc_args['fields'] ) ) { $body = self::limit_item_to_fields( $body, $fields ); } if ( 'headers' === $assoc_args['format'] ) { echo json_encode( $headers ); } elseif ( 'body' === $assoc_args['format'] ) { echo json_encode( $body ); } elseif ( 'envelope' === $assoc_args['format'] ) { echo json_encode( array( 'body' => $body, 'headers' => $headers, 'status' => $status, 'api_url' => $this->api_url, ) ); } else { $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $body ); } } /** * List all items. * * @subcommand list */ public function list_items( $args, $assoc_args ) { if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { $method = 'HEAD'; } else { $method = 'GET'; } list( $status, $body, $headers ) = $this->do_request( $method, $this->get_base_route(), $assoc_args ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_column( $body, 'id' ); } else { $items = $body; } if ( ! empty( $assoc_args['fields'] ) ) { foreach ( $items as $key => $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']; } elseif ( 'headers' === $assoc_args['format'] ) { echo json_encode( $headers ); } elseif ( 'body' === $assoc_args['format'] ) { echo json_encode( $body ); } elseif ( 'envelope' === $assoc_args['format'] ) { echo json_encode( array( 'body' => $body, 'headers' => $headers, 'status' => $status, 'api_url' => $this->api_url, ) ); } else { $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $items ); } } /** * Compare items between environments. * * <alias> * : Alias for the WordPress site to compare to. * * [<resource>] * : Limit comparison to a specific resource, instead of the collection. * * [--fields=<fields>] * : Limit comparison to specific fields. * * @subcommand diff */ public function diff_items( $args, $assoc_args ) { list( $alias ) = $args; if ( ! array_key_exists( $alias, \WP_CLI::get_runner()->aliases ) ) { \WP_CLI::error( "Alias '{$alias}' not found." ); } $resource = isset( $args[1] ) ? $args[1] : null; $fields = \WP_CLI\Utils\get_flag_value( $assoc_args, 'fields', null ); 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]; $other_args = implode( ' ', array_map( 'escapeshellarg', array( $alias, 'rest', $this->name, 'list' ) ) ); $other_assoc_args = \WP_CLI\Utils\assoc_args_to_str( array( 'format' => 'envelope' ) ); $full_command = "{$php_bin} {$script_path} {$other_args} {$other_assoc_args}"; $process = \WP_CLI\Process::create( $full_command, null, array( 'HOME' => getenv( 'HOME' ), 'WP_CLI_PACKAGES_DIR' => getenv( 'WP_CLI_PACKAGES_DIR' ), 'WP_CLI_CONFIG_PATH' => getenv( 'WP_CLI_CONFIG_PATH' ), ) ); $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_null( $resource ) ) { $field = is_numeric( $resource ) ? 'id' : 'slug'; $callback = function( $value ) use ( $field, $resource ) { if ( isset( $value[ $field ] ) && $resource == $value[ $field ] ) { return true; } return false; }; foreach ( array( 'to_body', 'from_body' ) as $response_type ) { $$response_type = array_filter( $$response_type, $callback ); } } $display_items = array(); do { $from_item = $to_item = array(); if ( ! empty( $from_body ) ) { $from_item = array_shift( $from_body ); if ( ! empty( $to_body ) && ! empty( $from_item['slug'] ) ) { foreach ( $to_body as $i => $item ) { if ( ! empty( $item['slug'] ) && $item['slug'] === $from_item['slug'] ) { $to_item = $item; unset( $to_body[ $i ] ); break; } } } } elseif ( ! empty( $to_body ) ) { $to_item = array_shift( $to_body ); } if ( ! empty( $to_item ) ) { foreach ( array( 'to_item', 'from_item' ) as $item ) { if ( isset( $$item['_links'] ) ) { unset( $$item['_links'] ); } } $display_items[] = array( 'from' => self::limit_item_to_fields( $from_item, $fields ), 'to' => self::limit_item_to_fields( $to_item, $fields ), ); } } while ( count( $from_body ) || count( $to_body ) ); \WP_CLI::line( \cli\Colors::colorize( "%R(-) {$this->api_url} %G(+) {$to_api_url}%n" ) ); foreach ( $display_items as $display_item ) { $this->show_difference( $this->name, array( 'from' => $display_item['from'], 'to' => $display_item['to'], ) ); } } /** * Update an existing item. * * @subcommand update */ public function update_item( $args, $assoc_args ) { list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args ); if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { \WP_CLI::line( $body['id'] ); } else { \WP_CLI::success( "Updated {$this->name} {$body['id']}." ); } } /** * Open an existing item in the editor * * @subcommand edit */ 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'] ) ) { \WP_CLI::error( 'Cannot edit - no schema found for resource.' ); } $schema = $options_body['schema']; list( $status, $resource_fields ) = $this->do_request( 'GET', $this->get_filled_route( $args ), $assoc_args ); $editable_fields = array(); foreach ( $resource_fields as $key => $value ) { if ( ! isset( $schema['properties'][ $key ] ) || ! empty( $schema['properties'][ $key ]['readonly'] ) ) { continue; } $properties = $schema['properties'][ $key ]; if ( isset( $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(); } $editable_fields[ $parent_key ][ $key ] = $value; } } continue; } if ( empty( $properties['readonly'] ) ) { $editable_fields[ $key ] = $value; } } if ( empty( $editable_fields ) ) { \WP_CLI::error( 'Cannot edit - no editable fields found on schema.' ); } $ret = \WP_CLI\Utils\launch_editor_for_input( \Spyc::YAMLDump( $editable_fields ), sprintf( 'Editing %s %s', $schema['title'], $args[0] ) ); if ( false === $ret ) { \WP_CLI::warning( 'No edits made.' ); } else { list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), \Spyc::YAMLLoadString( $ret ) ); \WP_CLI::success( "Updated {$schema['title']} {$args[0]}." ); } } /** * Do a REST Request * * @param string $method */ private function do_request( $method, $route, $assoc_args ) { if ( 'internal' === $this->scope ) { if ( ! defined( 'REST_REQUEST' ) ) { define( 'REST_REQUEST', true ); } $request = new \WP_REST_Request( $method, $route ); if ( in_array( $method, array( 'POST', 'PUT' ) ) ) { $request->set_body_params( $assoc_args ); } else { foreach ( $assoc_args as $key => $value ) { $request->set_param( $key, $value ); } } if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); } $response = rest_do_request( $request ); if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { $performed_queries = array(); foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { if ( in_array( $key, $original_queries ) ) { continue; } $performed_queries[] = $query; } usort( $performed_queries, function( $a, $b ) { if ( $a[1] === $b[1] ) { return 0; } return ( $a[1] > $b[1] ) ? -1 : 1; } ); $query_count = count( $performed_queries ); $query_total_time = 0; foreach ( $performed_queries as $query ) { $query_total_time += $query[1]; } $slow_query_message = ''; if ( $performed_queries && 'rest' === \WP_CLI::get_config( 'debug' ) ) { $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL; foreach ( $performed_queries as $i => $query ) { $i++; $bits = explode( ', ', $query[2] ); $backtrace = implode( ', ', array_slice( $bits, 13 ) ); $seconds = round( $query[1], 6 ); $slow_query_message .= <<<EOT {$i}: - {$seconds} seconds - {$backtrace} - {$query[0]} EOT; $slow_query_message .= PHP_EOL; } } elseif ( 'rest' !== \WP_CLI::get_config( 'debug' ) ) { $slow_query_message = '. Use --debug=rest to see all queries.'; } $query_total_time = round( $query_total_time, 6 ); \WP_CLI::debug( "REST command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'rest' ); } if ( $error = $response->as_error() ) { \WP_CLI::error( $error ); } return array( $response->get_status(), $response->get_data(), $response->get_headers() ); } elseif ( 'http' === $this->scope ) { $headers = array(); if ( ! empty( $this->auth ) && 'basic' === $this->auth['type'] ) { $headers['Authorization'] = 'Basic ' . base64_encode( $this->auth['username'] . ':' . $this->auth['password'] ); } if ( 'OPTIONS' === $method ) { $method = 'GET'; $assoc_args['_method'] = 'OPTIONS'; } $response = \WP_CLI\Utils\http_request( $method, rtrim( $this->api_url, '/' ) . $route, $assoc_args, $headers ); $body = json_decode( $response->body, true ); if ( $response->status_code >= 400 ) { if ( ! empty( $body['message'] ) ) { \WP_CLI::error( $body['message'] . ' ' . json_encode( array( 'status' => $response->status_code ) ) ); } else { switch ( $response->status_code ) { case 404: \WP_CLI::error( "No {$this->name} found." ); break; default: \WP_CLI::error( 'Could not complete request.' ); break; } } } return array( $response->status_code, json_decode( $response->body, true ), $response->headers->getAll() ); } \WP_CLI::error( 'Invalid scope for REST command.' ); } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return \WP_CLI\Formatter */ protected function get_formatter( &$assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { if ( is_string( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } else { $fields = $assoc_args['fields']; } } else { if ( ! empty( $assoc_args['context'] ) ) { $fields = $this->get_context_fields( $assoc_args['context'] ); } else { $fields = $this->get_context_fields( 'view' ); } } return new \WP_CLI\Formatter( $assoc_args, $fields ); } /** * Get a list of fields present in a given context * * @param string $context * @return array */ private function get_context_fields( $context ) { $fields = array(); foreach ( $this->schema['properties'] as $key => $args ) { if ( empty( $args['context'] ) || in_array( $context, $args['context'] ) ) { $fields[] = $key; } } return $fields; } /** * Get the base route for this resource * * @return string */ private function get_base_route() { return substr( $this->route, 0, strlen( $this->route ) - strlen( $this->resource_identifier ) ); } /** * Fill the route based on provided $args */ private function get_filled_route( $args ) { return rtrim( $this->get_base_route(), '/' ) . '/' . $args[0]; } /** * Visually depict the difference between "dictated" and "current" * * @param array */ private function show_difference( $slug, $difference ) { $this->output_nesting_level = 0; $this->nested_line( $slug . ': ' ); $this->recursively_show_difference( $difference['to'], $difference['from'] ); $this->output_nesting_level = 0; } /** * Recursively output the difference between "dictated" and "current" */ private function recursively_show_difference( $dictated, $current = null ) { $this->output_nesting_level++; if ( $this->is_assoc_array( $dictated ) ) { foreach ( $dictated as $key => $value ) { if ( $this->is_assoc_array( $value ) || is_array( $value ) ) { $new_current = isset( $current[ $key ] ) ? $current[ $key ] : null; if ( $new_current ) { $this->nested_line( $key . ': ' ); } else { $this->add_line( $key . ': ' ); } $this->recursively_show_difference( $value, $new_current ); } elseif ( is_string( $value ) ) { $pre = $key . ': '; if ( isset( $current[ $key ] ) && $current[ $key ] !== $value ) { $this->remove_line( $pre . $current[ $key ] ); $this->add_line( $pre . $value ); } elseif ( ! isset( $current[ $key ] ) ) { $this->add_line( $pre . $value ); } } } } elseif ( is_array( $dictated ) ) { foreach ( $dictated as $value ) { if ( ! $current || ! in_array( $value, $current ) ) { $this->add_line( '- ' . $value ); } } } elseif ( is_string( $value ) ) { $pre = $key . ': '; if ( isset( $current[ $key ] ) && $current[ $key ] !== $value ) { $this->remove_line( $pre . $current[ $key ] ); $this->add_line( $pre . $value ); } elseif ( ! isset( $current[ $key ] ) ) { $this->add_line( $pre . $value ); } else { $this->nested_line( $pre ); } } $this->output_nesting_level--; } /** * Output a line to be added * * @param string */ private function add_line( $line ) { $this->nested_line( $line, 'add' ); } /** * Output a line to be removed * * @param string */ private function remove_line( $line ) { $this->nested_line( $line, 'remove' ); } /** * Output a line that's appropriately nested */ private function nested_line( $line, $change = false ) { if ( 'add' == $change ) { $color = '%G'; $label = '+ '; } elseif ( 'remove' == $change ) { $color = '%R'; $label = '- '; } else { $color = false; $label = false; } $spaces = ( $this->output_nesting_level * 2 ) + 2; if ( $color && $label ) { $line = \cli\Colors::colorize( "{$color}{$label}" ) . $line . \cli\Colors::colorize( '%n' ); $spaces = $spaces - 2; } \WP_CLI::line( str_pad( ' ', $spaces ) . $line ); } /** * Whether or not this is an associative array * * @param array * @return bool */ private function is_assoc_array( $array ) { if ( ! is_array( $array ) ) { return false; } return array_keys( $array ) !== range( 0, count( $array ) - 1 ); } /** * Reduce an item to specific fields. * * @param array $item * @param array $fields * @return array */ private static function limit_item_to_fields( $item, $fields ) { if ( empty( $fields ) ) { return $item; } if ( is_string( $fields ) ) { $fields = explode( ',', $fields ); } foreach ( $item as $i => $field ) { if ( ! in_array( $i, $fields ) ) { unset( $item[ $i ] ); } } return $item; } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- __construct
- add_line — Output a line to be added
- create_item — Create a new item.
- delete_item — Delete an existing item.
- diff_items — Compare items between environments.
- do_request — Do a REST Request
- edit_item — Open an existing item in the editor
- generate_items — Generate some items.
- get_base_route — Get the base route for this resource
- get_context_fields — Get a list of fields present in a given context
- get_filled_route — Fill the route based on provided $args
- get_formatter — Get Formatter object based on supplied parameters.
- get_item — Get a single item.
- is_assoc_array — Whether or not this is an associative array
- limit_item_to_fields — Reduce an item to specific fields.
- list_items — List all items.
- nested_line — Output a line that's appropriately nested
- recursively_show_difference — Recursively output the difference between "dictated" and "current"
- remove_line — Output a line to be removed
- show_difference — Visually depict the difference between "dictated" and "current"
- update_item — Update an existing item.
Changelog Changelog
Version | Description |
---|---|
0.0.1 | Introduced. |