Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a917c45
Squash
adamziel May 6, 2020
fd9fd58
Refactor useNavigationBlocks
adamziel May 14, 2020
867bbfe
First version of functional batch saving
adamziel May 14, 2020
9ffd9bf
Call receiveEntityRecords with proper query
adamziel May 14, 2020
6ca5a93
Rename /save-hierarchy to /batch
adamziel May 14, 2020
7035927
Restore the original version of create_item function
adamziel May 14, 2020
e8310b3
Call proper hooks and actions during batch delete and update
adamziel May 14, 2020
3d018e5
Cleanup batch processing code
adamziel May 14, 2020
f7f7dcc
Remove MySQL transaction for now
adamziel May 14, 2020
0b31a5a
Add actions
adamziel May 14, 2020
e8df364
Clean up naming, add a few comments
adamziel May 14, 2020
5f22541
Add more documentation
adamziel May 14, 2020
b694254
sort menu items received from the API
adamziel May 14, 2020
c84044e
Simplify validate functions signatures
adamziel May 14, 2020
cfdae14
Restore the previous version of prepare_item_for_database
adamziel May 14, 2020
a5bc25b
Formatting
adamziel May 14, 2020
b31e14a
Formatting
adamziel May 14, 2020
9724cd6
Remove the Operation abstraction
adamziel May 14, 2020
aeb59e3
Formatting
adamziel May 14, 2020
9bfe04d
Remove additional input argument, use just request
adamziel May 14, 2020
35b9e70
Formatting
adamziel May 14, 2020
fc625f0
input->request
adamziel May 14, 2020
5bbe7f7
Provide information to the client about the specific input that cause…
adamziel May 14, 2020
e29b067
Clean pass through phpcs
adamziel May 15, 2020
ec19cb3
Clean pass through existing unit tests
adamziel May 15, 2020
0d6f9b1
Add initial unit test
adamziel May 15, 2020
e3e1844
Add a few more tests
adamziel May 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions lib/class-wp-rest-menu-items-batch-processor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<?php
/**
* REST API: WP_REST_Menu_Items_Batch_Processor class
*
* @package WordPress
* @subpackage REST_API
*/

/**
* Class that processes a batch of menu item updates and deletes.
*
* @see WP_REST_Posts_Controller
*/
class WP_REST_Menu_Items_Batch_Processor {

const UPDATE = 'update';
const DELETE = 'delete';

/**
* The ID of menu to process.
*
* @var int
*/
private $navigation_id;

/**
* Instance of parent WP_REST_Posts_Controller.
*
* @var WP_REST_Posts_Controller
*/
private $controller;

/**
* Full details about the request.
*
* @var WP_REST_Request
*/
private $request;

/**
* WP_REST_Menu_Items_Batch_Processor constructor.
*
* @param int $navigation_id The ID of menu to process.
* @param WP_REST_Menu_Items_Controller $controller Instance of parent WP_REST_Posts_Controller.
* @param WP_REST_Request $request Full details about the request.
*/
public function __construct( $navigation_id, WP_REST_Menu_Items_Controller $controller, $request ) {
global $wpdb;

$this->navigation_id = $navigation_id;
$this->request = $request;
$this->controller = $controller;
$this->wpdb = $wpdb;
}

/**
* Brings the stored menu items to the state described by $raw_input. Validates the entire
* input upfront and short-circuits if it's invalid.
*
* @param object $raw_input Raw input from the client - a tree of menu items that the user wants to persist.
*
* @return void|WP_Error Nothing on success, WP_Error carrying the data about specific input that caused the problem on failure
*/
public function process( $raw_input ) {
$batch = $this->compute_batch( $raw_input );
if ( is_wp_error( $batch ) ) {
return $batch;
}

$validated_batch = $this->validate_batch( $batch );
if ( is_wp_error( $validated_batch ) ) {
return $validated_batch;
}

do_action( 'menu_items_batch_processing_start', $this->navigation_id );

$result = $this->persist_batch( $validated_batch );

if ( is_wp_error( $result ) ) {
// We're in a broken state now, some operations succeeded and some other failed.
// This is okay for the experimental version 1.
// In the future let's wrap this in a transaction if WP tables are based on InnoDB
// and do something smart on rollback - e.g. try to restore the original state, or
// refresh all the caches that were affected in the process.
do_action( 'menu_items_batch_processing_failure', $this->navigation_id );

return $result;
}

do_action( 'menu_items_batch_processing_success', $this->navigation_id );

return $result;
}

/**
* Computes a list of updates and deletes necessary to reshape the current DB state into the one described by $input_tree.
*
* @param object $input_tree Raw input from the client - a tree of menu items that the user wants to persist.
*
* @return array|WP_Error List of operations on success, WP_Error carrying the data about specific input that caused the problem on failure.
*/
public function compute_batch( $input_tree ) {
$current_menu_items = $this->controller->get_menu_items( $this->navigation_id );
$operations = array();

$stack = array(
array( null, $input_tree ),
);
$updated_ids = array();

// Compute all necessary Updates.
while ( ! empty( $stack ) ) {
list( $parent_operation, $raw_menu_items ) = array_pop( $stack );
foreach ( $raw_menu_items as $n => $raw_menu_item ) {
$children = ! empty( $raw_menu_item['children'] ) ? $raw_menu_item['children'] : array();
unset( $raw_menu_item['children'] );
// Let's infer the menu order and parent id from the input tree.
$raw_menu_item['menu_order'] = $n + 1;
$raw_menu_item['parent'] = $parent_operation ? $parent_operation[1]['id'] : 0;

if ( ! empty( $raw_menu_item['id'] ) ) {
$updated_ids[] = $raw_menu_item['id'];
$operation = array( static::UPDATE, $raw_menu_item );
$operations[] = $operation;
} else {
// Inserts are slow so we don't allow them here. Instead they are handled "on the fly"
// by use-navigation-blocks.js so that this code may deal exclusively with the updates.
return new WP_Error( 'insert_unsupported', __( 'Cannot insert new items using batch processing.', 'gutenberg' ), array( 'status' => 400 ) );
}

if ( $children ) {
array_push( $stack, array( $operation, $children ) );
}
}
}

// Delete any orphaned items.
foreach ( $current_menu_items as $item ) {
if ( ! in_array( $item->ID, $updated_ids, true ) ) {
$operations[] = array(
static::DELETE,
array(
'menus' => $this->navigation_id,
'force' => true,
'id' => $item->ID,
),
);
}
}

return $operations;
}

/**
* Validates the list of operations from compute_batch.
*
* @param object $batch Output of compute_batch.
*
* @return array|WP_Error List of validated operations enriched with the database-ready arrays on success, WP_Error carrying the data about specific input that caused the problem on failure.
*/
public function validate_batch( $batch ) {
// We infer the menu order and parent id from the received input tree so there's no need
// to validate them in the controller.
foreach ( $batch as $k => list( $type, $input ) ) {
$request = new WP_REST_Request();
$request->set_default_params( $input );
$request->set_param( 'validate_order_and_hierarchy', false );
if ( static::UPDATE === $type ) {
$result = $this->controller->update_item_validate( $request );
} elseif ( static::DELETE === $type ) {
$result = $this->controller->delete_item_validate( $request );
}
if ( is_wp_error( $result ) ) {
$result->add_data( $input, 'input' );
return $result;
}
$batch[ $k ][] = $result;
}

return $batch;
}

/**
* Executes the operations prepared by compute_batch and validate_batch.
*
* @param object $validated_operations Output of batch_validate.
*
* @return void|WP_Error Nothing on success, WP_Error carrying the data about specific input that caused the problem on failure.
*/
public function persist_batch( $validated_operations ) {
foreach ( $validated_operations as $operation ) {
list( $type, $input, $prepared_nav_item ) = $operation;
$request = new WP_REST_Request();
$request->set_default_params( $input );
if ( static::UPDATE === $type ) {
$result = $this->controller->update_item_persist( $prepared_nav_item, $request );
} elseif ( static::DELETE === $type ) {
$result = $this->controller->delete_item_persist( $request );
}

if ( is_wp_error( $result ) ) {
$result->add_data( $input, 'input' );
return $result;
}

if ( static::UPDATE === $type ) {
$this->controller->update_item_notify( $result, $request );
} elseif ( static::DELETE === $type ) {
$this->controller->delete_item_notify( $result, new WP_REST_Response(), $request );
}
}
}

}
Loading