├── .gitignore ├── README.md ├── assets └── processor.js ├── changelog.txt ├── composer.json ├── composer.lock ├── examples ├── class-example-batch.php └── processing.gif ├── includes ├── class-batch-ajax-handler.php ├── class-batch-item.php ├── class-batch-list-table.php ├── class-batch-processor-admin.php ├── class-batch-processor.php ├── class-batch.php ├── class-bp-helper.php └── class-bp-singleton.php ├── views ├── batch-list.php ├── batch-manage.php └── batch-view.php └── wp-batch-processing.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | vendor/* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Batch Processing 2 | 3 | WP Batch Processing is WordPress plugin for creating batches of data and processing the data items one by one. It allows you to define a batch and to process the queued batch items one by one. There is also option to resume/continue later in case your internet connection goes down. 4 | 5 | ![Example](examples/processing.gif) 6 | 7 | ## Installation 8 | 9 | There are two ways to install this library: 10 | 11 | 1. Install this library as a plugin. It doesn't require anything from below or, 12 | 13 | 2. Install it as a composer package (read the **notes** bellow) 14 | 15 | ``` 16 | composer require gdarko/wp-batch-processing 17 | ``` 18 | 19 | ```php 20 | WP_Batch_Processor::boot(); 21 | ``` 22 | 23 | **Note**: The `boot()` method should be called only if you install the plugin via composer somewhere in your plugin or theme. 24 | 25 | **Note**: If using composer, the library will attempt to find its path, however if you see messed up screen it means that it was unable to find the stylesheet/JS files and you will need to define them manually before `boot()` method. 26 | 27 | ```php 28 | // Manually define the path constants to eliminate 29 | // possible errors when resolving the paths and also 30 | // include trailing slash at the end. 31 | 32 | if ( ! defined('WP_BP_PATH')) { 33 | define('WP_BP_PATH', '/path/to/wp-content/plugins/your-plugin/libraries/wp-batch-processing/'); 34 | } 35 | 36 | if ( ! defined('WP_BP_URL')) { 37 | define('WP_BP_URL', 'https://site.com/wp-content/plugins/your-plugin/libraries/wp-batch-processing/'); 38 | } 39 | 40 | WP_Batch_Processor::boot(); 41 | ``` 42 | 43 | 44 | ## How it works 45 | 46 | To define a batch you just need to extend the class `WP_Batch` and later register it. Follow the examples below to learn how. 47 | 48 | The class provides the following attributes and methods 49 | 50 | * `$id` - Identifies the batch (must be unique), 51 | * `$title` - Shown in the admin area, 52 | * `setup()` - Method that you use it to fed your data with `WP_Batch_Item` instances, 53 | * `process(WP_Batch_Item $item)` - Method that will be used to process each next item in the batch 54 | 55 | ```php 56 | 57 | if ( class_exists( 'WP_Batch' ) ) { 58 | 59 | /** 60 | * Class MY_Example_Batch 61 | */ 62 | class MY_Example_Batch extends WP_Batch { 63 | 64 | /** 65 | * Unique identifier of each batch 66 | * @var string 67 | */ 68 | public $id = 'email_post_authors'; 69 | 70 | /** 71 | * Describe the batch 72 | * @var string 73 | */ 74 | public $title = 'Email Post Authors'; 75 | 76 | /** 77 | * To setup the batch data use the push() method to add WP_Batch_Item instances to the queue. 78 | * 79 | * Note: If the operation of obtaining data is expensive, cache it to avoid slowdowns. 80 | * 81 | * @return void 82 | */ 83 | public function setup() { 84 | 85 | $users = get_users( array( 86 | 'number' => '40', 87 | 'role' => 'author', 88 | ) ); 89 | 90 | foreach ( $users as $user ) { 91 | $this->push( new WP_Batch_Item( $user->ID, array( 'author_id' => $user->ID ) ) ); 92 | } 93 | } 94 | 95 | /** 96 | * Handles processing of batch item. One at a time. 97 | * 98 | * In order to work it correctly you must return values as follows: 99 | * 100 | * - TRUE - If the item was processed successfully. 101 | * - WP_Error instance - If there was an error. Add message to display it in the admin area. 102 | * 103 | * @param WP_Batch_Item $item 104 | * 105 | * @return bool|\WP_Error 106 | */ 107 | public function process( $item ) { 108 | 109 | // Retrieve the custom data 110 | $author_id = $item->get_value( 'author_id' ); 111 | 112 | // Return WP_Error if the item processing failed (In our case we simply skip author with user id 5) 113 | if ( $author_id == 5 ) { 114 | return new WP_Error( 302, "Author skipped" ); 115 | } 116 | 117 | // Do the expensive processing here. 118 | // ... 119 | 120 | // Return true if the item processing is successful. 121 | return true; 122 | } 123 | 124 | /** 125 | * Called when specific process is finished (all items were processed). 126 | * This method can be overriden in the process class. 127 | * @return void 128 | */ 129 | public function finish() { 130 | // Do something after process is finished. 131 | // You have $this->items, etc. 132 | } 133 | } 134 | } 135 | 136 | ``` 137 | 138 | After creating the class, class instance needs to be registered in order to be available in the batches list in the admin area. 139 | 140 | ```php 141 | /** 142 | * Initialize the batches. 143 | */ 144 | function wp_batch_processing_init() { 145 | $batch = new MY_Example_Batch(); 146 | WP_Batch_Processor::get_instance()->register( $batch ); 147 | } 148 | add_action( 'wp_batch_processing_init', 'wp_batch_processing_init', 15, 1 ); 149 | ``` 150 | 151 | That's it. 152 | 153 | ## Filters and Actions 154 | 155 | Set delay between processing items. Default is 0 (no delay) 156 | ```php 157 | function wp_bp_my_custom_delay($delay) { 158 | return 2; // in seconds 159 | } 160 | add_filter('wp_batch_processing_delay', 'wp_bp_my_custom_delay', 10, 1); 161 | ``` 162 | 163 | ## Example use cases 164 | 165 | The tool can be used in many different ways. For example 166 | 167 | * Importing data 168 | * Downloading data 169 | * Emailing 170 | * Database modifications 171 | 172 | 173 | ## Contribute 174 | 175 | If you notice a bug or you want to propose improvements feel free to create a pull request! 176 | 177 | 178 | ## License 179 | 180 | The plugin is licensed under GPL v2 181 | 182 | ``` 183 | Copyright (C) 2021 Darko Gjorgjijoski (https://darkog.com) 184 | 185 | This file is part of WP Batch Processing 186 | 187 | WP Batch Processing is free software: you can redistribute it and/or modify 188 | it under the terms of the GNU General Public License as published by 189 | the Free Software Foundation, either version 2 of the License, or 190 | (at your option) any later version. 191 | 192 | WP Batch Processing is distributed in the hope that it will be useful, 193 | but WITHOUT ANY WARRANTY; without even the implied warranty of 194 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 195 | GNU General Public License for more details. 196 | 197 | You should have received a copy of the GNU General Public License 198 | along with WP Batch Processing. If not, see . 199 | ``` 200 | -------------------------------------------------------------------------------- /assets/processor.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var reload = function() { 4 | window.location.href = window.location.href; 5 | }; 6 | 7 | var stop_process = function() { 8 | reload(); 9 | }; 10 | 11 | var restart_batch = function() { 12 | var nonce = DgBatchRunner.nonce; 13 | var batch = DgBatchRunner.batch_id; 14 | var ajax_url = DgBatchRunner.ajax_url; 15 | $.ajax({ 16 | url: ajax_url + '?action=dg_restart_batch&nonce='+nonce, 17 | type: 'POST', 18 | data: {batch_id: batch}, 19 | cache: false, 20 | success: function(response) { 21 | reload(); 22 | } 23 | }); 24 | }; 25 | 26 | var process_next_item = function() { 27 | var nonce = DgBatchRunner.nonce; 28 | var batch = DgBatchRunner.batch_id; 29 | var ajax_url = DgBatchRunner.ajax_url; 30 | var delay = DgBatchRunner.delay; 31 | $.ajax({ 32 | url: ajax_url + '?action=dg_process_next_batch_item&nonce='+nonce, 33 | type: 'POST', 34 | data: {batch_id: batch}, 35 | cache: false, 36 | beforeSend: function() { 37 | $('#batch-process-start').text(DgBatchRunner.text.processing).prop('disabled', true); 38 | }, 39 | success: function(response) { 40 | $(document).trigger('itemprocessed', [response]) 41 | if(!response.data.is_finished) { 42 | if(delay > 0) { 43 | console.log('Waiting ' + DgBatchRunner.delay + ' seconds before processing next item.'); 44 | setTimeout(function(){ 45 | process_next_item(); 46 | }, delay * 1000); 47 | } else { 48 | process_next_item(); 49 | } 50 | } else { 51 | $('#batch-process-start').text(DgBatchRunner.text.start); 52 | } 53 | }, 54 | error: function() { 55 | alert('HTTP Error.'); 56 | }, 57 | }); 58 | 59 | } 60 | 61 | $(document).on('itemprocessed', function(e, response){ 62 | var percentage = response.data.percentage; 63 | $('.batch-process-progress-bar-inner').css('width', percentage+'%'); 64 | $('#batch-process-total').text(response.data.total_items); 65 | $('#batch-process-processed').text(response.data.total_processed); 66 | $('#batch-process-percentage').text('('+percentage+'%)'); 67 | var color = response.success ? 'green' : 'red'; 68 | var message = ''+response.data.message+''; 69 | $('.batch-process-current-item').html(message); 70 | if(response.data.is_finished) { 71 | $('#batch-process-start, #batch-process-stop').prop('disabled', true); 72 | } 73 | if(!response.success) { 74 | $('#batch-errors').show(); 75 | $('#batch-errors-list').append('
  • '+response.data.message+'
  • '); 76 | } 77 | }); 78 | 79 | $(document).on('click', '#batch-process-start', function(e){ 80 | e.preventDefault(); 81 | process_next_item(); 82 | }); 83 | 84 | $(document).on('click', '#batch-process-stop', function(e){ 85 | e.preventDefault(); 86 | stop_process(); 87 | }); 88 | 89 | $(document).on('click', '#batch-process-restart', function(e){ 90 | e.preventDefault(); 91 | if(confirm(DgBatchRunner.text.confirm_restart)) { 92 | restart_batch(); 93 | } 94 | }); 95 | 96 | 97 | })(jQuery); -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | = Version 1.0.1 (2019-07-14) = 2 | - [New] Added finish() method that can be overriden in the derived classes. It is called when specific batch finished processing. 3 | - [Fix] Division by zero warnings 4 | 5 | = Version 1.0.0 (2019-06-30) = 6 | - Initial version 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gdarko/wp-batch-processing", 3 | "description": "Easily process large batches of data in WordPress. Provide the data, setup the processing procedure, run the batch processor from the admin dashboard. Profit.", 4 | "type": "library", 5 | "require": { 6 | "php": ">=5.3" 7 | }, 8 | "license": "GPL-2.0-or-later", 9 | "authors": [ 10 | { 11 | "name": "Darko Gjorgjijoski", 12 | "email": "dg@darkog.com" 13 | } 14 | ], 15 | "autoload": { 16 | "classmap": [ 17 | "includes/" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "fac3bfb16f48815eb401dd55eefbde96", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.3" 17 | }, 18 | "platform-dev": [], 19 | "plugin-api-version": "2.1.0" 20 | } 21 | -------------------------------------------------------------------------------- /examples/class-example-batch.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | if ( class_exists( 'WP_Batch' ) ) { 26 | 27 | /** 28 | * Class MY_Example_Batch 29 | */ 30 | class MY_Example_Batch extends WP_Batch { 31 | 32 | /** 33 | * Unique identifier of each batch 34 | * @var string 35 | */ 36 | public $id = 'email_post_authors'; 37 | 38 | /** 39 | * Describe the batch 40 | * @var string 41 | */ 42 | public $title = 'Email Post Authors'; 43 | 44 | /** 45 | * To setup the batch data use the push() method to add WP_Batch_Item instances to the queue. 46 | * 47 | * Note: If the operation of obtaining data is expensive, cache it to avoid slowdowns. 48 | * 49 | * @return void 50 | */ 51 | public function setup() { 52 | 53 | $users = get_users( array( 54 | 'number' => '40', 55 | 'role' => 'author', 56 | ) ); 57 | 58 | foreach ( $users as $user ) { 59 | $this->push( new WP_Batch_Item( $user->ID, array( 'author_id' => $user->ID ) ) ); 60 | } 61 | } 62 | 63 | /** 64 | * Handles processing of batch item. One at a time. 65 | * 66 | * In order to work it correctly you must return values as follows: 67 | * 68 | * - TRUE - If the item was processed successfully. 69 | * - WP_Error instance - If there was an error. Add message to display it in the admin area. 70 | * 71 | * @param WP_Batch_Item $item 72 | * 73 | * @return bool|\WP_Error 74 | */ 75 | public function process( $item ) { 76 | 77 | // Retrieve the custom data 78 | $author_id = $item->get_value( 'author_id' ); 79 | 80 | // Return WP_Error if the item processing failed (In our case we simply skip author with user id 5) 81 | if ( $author_id == 5 ) { 82 | return new WP_Error( 302, "Author skipped" ); 83 | } 84 | 85 | // Do the expensive processing here. eg. Sending email. 86 | // ... 87 | 88 | // Return true if the item processing is successful. 89 | return true; 90 | } 91 | 92 | /** 93 | * Called when specific process is finished (all items were processed). 94 | * This method can be overriden in the process class. 95 | * @return void 96 | */ 97 | public function finish() { 98 | // Do something after process is finished. 99 | // You have $this->items, or other data you can set. 100 | } 101 | 102 | } 103 | 104 | /** 105 | * Initialize the batches. 106 | */ 107 | function wp_batch_processing_init() { 108 | $batch = new MY_Example_Batch(); 109 | WP_Batch_Processor::get_instance()->register( $batch ); 110 | } 111 | 112 | add_action( 'wp_batch_processing_init', 'wp_batch_processing_init', 15, 1 ); 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /examples/processing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdarko/wp-batch-processing/be2a7a7b30423df09a4dd1c8098c71fa249d49dd/examples/processing.gif -------------------------------------------------------------------------------- /includes/class-batch-ajax-handler.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | /** 26 | * Class WP_Batch_Processing_Ajax_Handler 27 | */ 28 | class WP_Batch_Processing_Ajax_Handler { 29 | 30 | use WP_BP_Singleton; 31 | 32 | /** 33 | * Setup the ajax endpoints 34 | */ 35 | protected function init() { 36 | add_action( 'wp_ajax_dg_process_next_batch_item', array( $this, 'process_next_item' ) ); 37 | add_action( 'wp_ajax_dg_restart_batch', array( $this, 'restart_batch' ) ); 38 | } 39 | 40 | /** 41 | * This is used to handle the processing of each item 42 | * and return the status to inform the user. 43 | */ 44 | public function process_next_item() { 45 | 46 | // Check ajax referrer 47 | if ( ! check_ajax_referer( WP_Batch_Processor_Admin::NONCE, 'nonce', false ) ) { 48 | wp_send_json_error( array( 49 | 'message' => 'Permission denied.', 50 | ) ); 51 | exit(); 52 | } 53 | 54 | // Validate the batch id. 55 | $batch_id = isset( $_REQUEST['batch_id'] ) ? $_REQUEST['batch_id'] : false; 56 | if ( ! $batch_id ) { 57 | wp_send_json_error( array( 58 | 'message' => 'Invalid batch id', 59 | ) ); 60 | exit(); 61 | } 62 | 63 | // Get the batch object 64 | $batch = WP_Batch_Processor::get_instance()->get_batch( $batch_id ); 65 | 66 | // Process the next item. 67 | $next_item = $batch->get_next_item(); 68 | 69 | // No next item for processing. The batch processing is finished, probably. 70 | $is_finished = ( false === $next_item ); 71 | 72 | if ( $is_finished ) { 73 | $total_processed = $batch->get_processed_count(); 74 | $total_items = $batch->get_items_count(); 75 | $percentage = $batch->get_percentage(); 76 | $batch->finish(); 77 | wp_send_json_success( array( 78 | 'message' => apply_filters( 'dg_batch_item_finished_message', __( 'Processing finished.', 'wp-batch-processing' ) ), 79 | 'is_finished' => 1, 80 | 'total_processed' => $total_processed, 81 | 'total_items' => $total_items, 82 | 'percentage' => $percentage, 83 | ) ); 84 | } else { 85 | @set_time_limit( 0 ); 86 | $response = $batch->process( $next_item ); 87 | $batch->mark_as_processed( $next_item->id ); 88 | $total_processed = $batch->get_processed_count(); 89 | $total_items = $batch->get_items_count(); 90 | $percentage = $batch->get_percentage(); 91 | if ( is_wp_error( $response ) ) { 92 | $error_message = apply_filters( 'dg_batch_item_error_message', 'Error processing item with id ' . $next_item->id . ': ' . $response->get_error_message(), $next_item ); 93 | wp_send_json_error( array( 94 | 'message' => $error_message, 95 | 'is_finished' => 0, 96 | 'total_processed' => $total_processed, 97 | 'total_items' => $total_items, 98 | 'percentage' => $percentage, 99 | ) ); 100 | } else { 101 | $success_message = apply_filters( 'dg_batch_item_success_message', 'Processed item with id ' . $next_item->id, $next_item ); 102 | wp_send_json_success( array( 103 | 'message' => $success_message, 104 | 'is_finished' => 0, 105 | 'total_processed' => $total_processed, 106 | 'total_items' => $total_items, 107 | 'percentage' => $percentage, 108 | ) ); 109 | } 110 | exit; 111 | 112 | } 113 | } 114 | 115 | /** 116 | * Used to restart the batch. 117 | * Just clear the data. 118 | */ 119 | public function restart_batch() { 120 | // Check ajax referrer 121 | if ( ! check_ajax_referer( WP_Batch_Processor_Admin::NONCE, 'nonce', false ) ) { 122 | wp_send_json_error( array( 123 | 'message' => 'Permission denied.', 124 | ) ); 125 | exit; 126 | } 127 | // Validate the batch id. 128 | $batch_id = isset( $_REQUEST['batch_id'] ) ? $_REQUEST['batch_id'] : false; 129 | if ( ! $batch_id ) { 130 | wp_send_json_error( array( 131 | 'message' => 'Invalid batch id', 132 | ) ); 133 | exit; 134 | } 135 | // Get the batch object 136 | $batch = WP_Batch_Processor::get_instance()->get_batch( $batch_id ); 137 | // Restart the batch. 138 | $batch->restart(); 139 | // Send json 140 | wp_send_json_success(); 141 | } 142 | } 143 | 144 | -------------------------------------------------------------------------------- /includes/class-batch-item.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | /** 26 | * Class WP_Batch_Item 27 | */ 28 | class WP_Batch_Item { 29 | 30 | /** 31 | * Unique identifier of the batch item 32 | * @var int 33 | */ 34 | public $id; 35 | 36 | /** 37 | * Additional data for the item 38 | * @var mixed 39 | */ 40 | public $data; 41 | 42 | /** 43 | * WP_Batch_Item constructor 44 | * 45 | * @param $id 46 | * @param $data 47 | */ 48 | public function __construct($id, $data = null) { 49 | $this->id = $id; 50 | $this->data = $data; 51 | } 52 | 53 | /** 54 | * Return data value 55 | * 56 | * @param $key 57 | * @param null $default 58 | * 59 | * @return mixed|null 60 | */ 61 | public function get_value($key, $default = null) { 62 | return isset($this->data[$key]) ? $this->data[$key] : $default; 63 | } 64 | } -------------------------------------------------------------------------------- /includes/class-batch-list-table.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | if ( ! class_exists( 'WP_List_Table' ) ) { 26 | require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; 27 | } 28 | 29 | /** 30 | * Class WP_BP_List_Table 31 | */ 32 | class WP_BP_List_Table extends \WP_List_Table { 33 | 34 | function __construct() { 35 | parent::__construct( array( 36 | 'singular' => 'batch', 37 | 'plural' => 'batches', 38 | 'ajax' => false 39 | ) ); 40 | } 41 | 42 | function get_table_classes() { 43 | return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] ); 44 | } 45 | 46 | /** 47 | * Message to show if no designation found 48 | * 49 | * @return void 50 | */ 51 | function no_items() { 52 | _e( 'No batches found. Read the documentation on the plugin github page to see how to register ones.', 'wp-batch-processing' ); 53 | } 54 | 55 | /** 56 | * Default column values if no callback found 57 | * 58 | * @param object $item 59 | * @param string $column_name 60 | * 61 | * @return string 62 | */ 63 | function column_default( $item, $column_name ) { 64 | 65 | switch ( $column_name ) { 66 | case 'title': 67 | return $item->title; 68 | 69 | case 'total_processed': 70 | return $item->get_processed_count(); 71 | 72 | case 'total_items': 73 | return $item->get_items_count(); 74 | 75 | default: 76 | return isset( $item->$column_name ) ? $item->$column_name : ''; 77 | } 78 | } 79 | 80 | /** 81 | * Get the column names 82 | * 83 | * @return array 84 | */ 85 | function get_columns() { 86 | $columns = array( 87 | 'title' => __( 'Title', 'wp-batch-processing' ), 88 | 'total_processed' => __( 'Total Processed', 'wp-batch-processing' ), 89 | 'total_items' => __( 'Total Items', 'wp-batch-processing' ), 90 | ); 91 | 92 | return $columns; 93 | } 94 | 95 | /** 96 | * Render the designation name column 97 | * 98 | * @param object $item 99 | * 100 | * @return string 101 | */ 102 | function column_title( $item ) { 103 | 104 | $actions = array(); 105 | $actions['edit'] = sprintf( '%s', admin_url( 'admin.php?page=dg-batches&action=view&id=' . $item->id ), $item->id, __( 'Manage Batch', 'wp-batch-processing' ), __( 'Manage', 'wp-batch-processing' ) ); 106 | 107 | return sprintf( '%2$s %3$s', admin_url( 'admin.php?page=dg-batches&action=view&id=' . $item->id ), $item->title, $this->row_actions( $actions ) ); 108 | } 109 | 110 | /** 111 | * Get sortable columns 112 | * 113 | * @return array 114 | */ 115 | function get_sortable_columns() { 116 | $sortable_columns = array(); 117 | 118 | return $sortable_columns; 119 | } 120 | 121 | /** 122 | * Set the bulk actions 123 | * 124 | * @return array 125 | */ 126 | function get_bulk_actions() { 127 | $actions = array(); 128 | 129 | return $actions; 130 | } 131 | 132 | /** 133 | * Render the checkbox column 134 | * 135 | * @param object $item 136 | * 137 | * @return string 138 | */ 139 | function column_cb( $item ) { 140 | return ''; 141 | } 142 | 143 | /** 144 | * Prepare the class items 145 | * 146 | * @return void 147 | */ 148 | function prepare_items() { 149 | 150 | $columns = $this->get_columns(); 151 | $hidden = array(); 152 | $sortable = $this->get_sortable_columns(); 153 | $this->_column_headers = array( $columns, $hidden, $sortable ); 154 | $this->page_status = isset( $_GET['status'] ) ? sanitize_text_field( $_GET['status'] ) : '2'; 155 | 156 | $this->items = WP_Batch_Processor::get_instance()->get_batches(); 157 | } 158 | } -------------------------------------------------------------------------------- /includes/class-batch-processor-admin.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | /** 26 | * Class WP_Batch_Processor_Admin 27 | */ 28 | class WP_Batch_Processor_Admin { 29 | 30 | use WP_BP_Singleton; 31 | 32 | const NONCE = 'wp-batch-processing'; 33 | 34 | /** 35 | * Kick-in the class 36 | */ 37 | protected function init() { 38 | add_action( 'admin_menu', array( $this, 'admin_menu' ) ); 39 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 40 | add_action( 'init', array( $this, 'setup' ), 0 ); 41 | } 42 | 43 | /** 44 | * Init hook. Only run when it is on its own pages. 45 | */ 46 | public function setup() { 47 | if ( $this->is_batch_runner_screen() || $this->is_batch_runner_ajax() ) { 48 | do_action( 'wp_batch_processing_init' ); 49 | } 50 | } 51 | 52 | /** 53 | * Enqueues admin scripts 54 | * 55 | * @return void 56 | */ 57 | public function enqueue_scripts() { 58 | 59 | if ( ! $this->is_batch_runner_screen( 'view' ) ) { 60 | return; 61 | } 62 | 63 | wp_enqueue_script( 64 | 'wp-batch-processing', 65 | WP_BP_URL . 'assets/processor.js', 66 | array( 'jquery' ), 67 | filemtime( WP_BP_PATH . 'assets/processor.js' ), 68 | true 69 | ); 70 | wp_localize_script( 'wp-batch-processing', 'DgBatchRunner', array( 71 | 'ajax_url' => admin_url( 'admin-ajax.php' ), 72 | 'nonce' => wp_create_nonce( 'wp-batch-processing' ), 73 | 'batch_id' => isset( $_GET['id'] ) ? $_GET['id'] : 0, 74 | 'delay' => apply_filters( 'wp_batch_processing_delay', 0 ), 75 | // Set delay in seconds before processing the next item. Default 0. No delay. 76 | 'text' => array( 77 | 'processing' => __( 'Processing...', 'wp-batch-processing' ), 78 | 'start' => __( 'Start', 'wp-batch-processing' ), 79 | 'confirm_restart' => __('Are you sure you want to restart? This action can not be reverted.', 'wp-batch-processing') 80 | ) 81 | ) ); 82 | } 83 | 84 | /** 85 | * Add menu items 86 | * 87 | * @return void 88 | */ 89 | public function admin_menu() { 90 | 91 | add_menu_page( 92 | __( 'Manage Batches', 'wp-batch-processing' ), 93 | __( 'Batches ', 'wp-batch-processing' ), 94 | 'manage_options', 'dg-batches', 95 | array( $this, 'plugin_page' ), 96 | 'dashicons-grid-view', null 97 | ); 98 | 99 | add_submenu_page( 100 | 'dg-batches', 101 | __( 'Batches', 'wp-batch-processing' ), 102 | __( 'Batches', 'wp-batch-processing' ), 103 | 'manage_options', 104 | 'dg-batches', 105 | array( $this, 'plugin_page' ) 106 | ); 107 | } 108 | 109 | /** 110 | * Handles the plugin page 111 | * 112 | * @return void 113 | */ 114 | public function plugin_page() { 115 | $action = isset( $_GET['action'] ) ? $_GET['action'] : 'list'; 116 | $id = isset( $_GET['id'] ) ? $_GET['id'] : 0; 117 | switch ( $action ) { 118 | case 'view': 119 | $view = 'batch-view'; 120 | break; 121 | default: 122 | $view = 'batch-list'; 123 | break; 124 | } 125 | WP_BP_Helper::render( $view, array( 'id' => $id ) ); 126 | } 127 | 128 | /** 129 | * Returns true if the it is ajax action 130 | * @return bool 131 | */ 132 | private function is_batch_runner_ajax() { 133 | return isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'dg_process_next_batch_item' || isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'dg_restart_batch'; 134 | } 135 | 136 | /** 137 | * Returns true if the current screen is batch runners one. 138 | * @return bool 139 | */ 140 | private function is_batch_runner_screen( $action = null ) { 141 | $is_main_screen = isset( $_GET['page'] ) && $_GET['page'] === 'dg-batches'; 142 | if ( ! is_null( $action ) ) { 143 | $is_main_screen = $is_main_screen && isset( $_GET['action'] ) && $_GET['action'] === $action; 144 | } 145 | 146 | return $is_main_screen; 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /includes/class-batch-processor.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | 26 | /** 27 | * Class WP_Batch_Processor 28 | */ 29 | class WP_Batch_Processor { 30 | 31 | use WP_BP_Singleton; 32 | 33 | /** 34 | * List of batches 35 | * @var WP_Batch[] 36 | */ 37 | protected $batches; 38 | 39 | /** 40 | * Initializes the runner 41 | */ 42 | public function init() { 43 | $this->batches = array(); 44 | } 45 | 46 | 47 | /** 48 | * Register batch 49 | * 50 | * @param WP_Batch $batch 51 | */ 52 | public function register( $batch ) { 53 | $this->batches[] = $batch; 54 | } 55 | 56 | /** 57 | * Returns array of registered batches 58 | * @return WP_Batch[] 59 | */ 60 | public function get_batches() { 61 | return $this->batches; 62 | } 63 | 64 | /** 65 | * Returns a batch from the registered ones. 66 | * 67 | * @param $id 68 | * 69 | * @return null|WP_Batch 70 | */ 71 | public function get_batch( $id ) { 72 | foreach ( $this->batches as $batch ) { 73 | if ( $batch->id === $id ) { 74 | return $batch; 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * Boot the plugin 83 | */ 84 | public static function boot() { 85 | self::load_paths(); 86 | WP_Batch_Processor_Admin::get_instance(); 87 | WP_Batch_Processing_Ajax_Handler::get_instance(); 88 | WP_Batch_Processor::get_instance(); 89 | } 90 | 91 | /** 92 | * Determine the library URL. 93 | * Note: This won't work if the library is outside of the wp-content directory 94 | * and also contains multiple 'wp-content' words in the path. 95 | */ 96 | private static function load_paths() { 97 | if ( ! defined( 'WP_BP_PATH' ) || ! defined( 'WP_BP_URL' ) ) { 98 | $path = trailingslashit( dirname( dirname( __FILE__ ) ) ); 99 | $content_dir = basename( untrailingslashit( WP_CONTENT_DIR ) ); 100 | $library_uri = substr( strstr( trailingslashit( $path ), $content_dir ), strlen( $content_dir ) ); 101 | $url = untrailingslashit( WP_CONTENT_URL ) . $library_uri; 102 | if ( ! defined( 'WP_BP_PATH' ) ) { 103 | define( 'WP_BP_PATH', $path ); 104 | } 105 | if ( ! defined( 'WP_BP_URL' ) ) { 106 | define( 'WP_BP_URL', trailingslashit( $url ) ); 107 | } 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /includes/class-batch.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | /** 26 | * Class WP_Batch 27 | * 28 | * Extend this class to create your own batch 29 | * 30 | * Note: You must register the instance in the wp_batch_processing_init hook 31 | * in order to show in the admin area. 32 | * 33 | * eg. WP_Batch_Processor::get_instance()->register( $batch ); 34 | */ 35 | abstract class WP_Batch { 36 | 37 | /** 38 | * Unique identifier of each batch 39 | * @var string 40 | */ 41 | public $id = 'my_first_batch'; 42 | 43 | /** 44 | * Describe the batch 45 | * @var string 46 | */ 47 | public $title = 'My first batch'; 48 | 49 | /** 50 | * Data store of batch items 51 | * @var WP_Batch_Item[] 52 | */ 53 | protected $items = array(); 54 | 55 | /** 56 | * Initialize 57 | */ 58 | public function __construct() { 59 | $this->setup(); 60 | } 61 | 62 | /** 63 | * To setup the batch data use the push() method to add WP_Batch_Item instances to the queue. 64 | * 65 | * Note: If the operation of obtaining data is expensive, cache it to avoid slowdowns. 66 | * 67 | * @return void 68 | */ 69 | abstract public function setup(); 70 | 71 | /** 72 | * Handles processing of batch item. One at a time. 73 | * 74 | * In order to work it correctly you must return values as follows: 75 | * 76 | * - TRUE - If the item was processed successfully. 77 | * - WP_Error instance - If there was an error. Add message to display it in the admin area. 78 | * 79 | * @param WP_Batch_Item $item 80 | * 81 | * @return bool|\WP_Error 82 | */ 83 | abstract public function process( $item ); 84 | 85 | /** 86 | * Called when specific process is finished (all items were processed). 87 | * This method can be overriden in the process class. 88 | * @return void 89 | */ 90 | public function finish() {} 91 | 92 | /** 93 | * Queues the item for processing. 94 | * 95 | * @param WP_Batch_Item $item 96 | */ 97 | protected function push( $item ) { 98 | if ( ! is_array( $this->items ) ) { 99 | $this->items = array(); 100 | } 101 | $this->items[] = $item; 102 | } 103 | 104 | 105 | /** 106 | * Returns false if no items left for processing or WP_Batch_Item object for processing. 107 | * 108 | * @param WP_Batch_Item $item 109 | * 110 | * @return bool|WP_Batch_Item 111 | */ 112 | public function get_next_item() { 113 | $processed = $this->get_processed_items(); 114 | foreach ( $this->items as $item ) { 115 | if ( ! in_array( $item->id, $processed ) ) { 116 | return $item; 117 | } 118 | } 119 | 120 | return false; 121 | } 122 | 123 | /** 124 | * Check if the batch is finished. 125 | * @return bool 126 | */ 127 | public function is_finished() { 128 | $is_finished = true; 129 | foreach ( $this->items as $item ) { 130 | if ( ! $this->is_processed( $item ) ) { 131 | $is_finished = false; 132 | } 133 | } 134 | 135 | return $is_finished; 136 | } 137 | 138 | 139 | /** 140 | * Check if batch item was processed. 141 | * 142 | * @param WP_Batch_Item $item 143 | * 144 | * @return bool 145 | */ 146 | public function is_processed( $item ) { 147 | return in_array( $item->id, $this->get_processed_items() ); 148 | } 149 | 150 | /** 151 | * Returns processed batch item ids 152 | * @return array 153 | */ 154 | public function get_processed_items() { 155 | $processed = get_option( $this->get_db_identifier(), array() ); 156 | 157 | return $processed; 158 | } 159 | 160 | /** 161 | * Returns the count of the processed items 162 | * @return int 163 | */ 164 | public function get_processed_count() { 165 | return count( $this->get_processed_items() ); 166 | } 167 | 168 | /** 169 | * Mark specific id as processed. 170 | * 171 | * @param int $id 172 | */ 173 | public function mark_as_processed( $id ) { 174 | $processed = $this->get_processed_items(); 175 | array_push( $processed, $id ); 176 | $processed = array_unique( $processed ); 177 | update_option( $this->get_db_identifier(), $processed ); 178 | } 179 | 180 | /** 181 | * Returns the count of the total items 182 | * @return int 183 | */ 184 | public function get_items_count() { 185 | return count( $this->items ); 186 | } 187 | 188 | /** 189 | * Returns the percentage 190 | * @return float 191 | */ 192 | public function get_percentage() { 193 | $total_items = $this->get_items_count(); 194 | $total_processed = $this->get_processed_count(); 195 | $percentage = ( ! empty( $total_items ) ) ? 100 - ( ( ( $total_items - $total_processed ) / $total_items ) * 100 ) : 0; 196 | 197 | return number_format( (float) $percentage, 2, '.', '' ); 198 | } 199 | 200 | /** 201 | * Returns the batch wp_options option_name identifier. 202 | * @return string 203 | */ 204 | public function get_db_identifier() { 205 | return 'batch_' . $this->id . '_processed'; 206 | } 207 | 208 | /** 209 | * Restarts the processed items store 210 | */ 211 | public function restart() { 212 | delete_option( $this->get_db_identifier() ); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /includes/class-bp-helper.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | /** 26 | * Class WP_BP_Helper 27 | */ 28 | class WP_BP_Helper { 29 | 30 | /** 31 | * Renders a view 32 | * 33 | * @param $view 34 | * @param array $data 35 | */ 36 | public static function render( $view, $data = array() ) { 37 | $path = WP_BP_PATH . 'views' . DIRECTORY_SEPARATOR . $view . '.php'; 38 | if ( file_exists( $path ) ) { 39 | if ( ! empty( $data ) ) { 40 | extract( $data ); 41 | } 42 | include( $path ); 43 | } else { 44 | echo 'View ' . $view . ' not found'; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /includes/class-bp-singleton.php: -------------------------------------------------------------------------------- 1 | . 19 | **********************************************************************/ 20 | 21 | if ( ! defined( 'ABSPATH' ) ) { 22 | die( 'Direct access is not allowed.' ); 23 | } 24 | 25 | trait WP_BP_Singleton { 26 | /** 27 | * The current instance 28 | */ 29 | protected static $instance; 30 | 31 | /** 32 | * Returns the current instance 33 | */ 34 | final public static function get_instance() { 35 | return isset( static::$instance ) 36 | ? static::$instance 37 | : static::$instance = new static; 38 | } 39 | 40 | /** 41 | * WP_BP_Singleton constructor. 42 | */ 43 | final private function __construct() { 44 | $this->init(); 45 | } 46 | 47 | // Prevent instances 48 | protected function init() { 49 | } 50 | 51 | public function __wakeup() { 52 | } 53 | 54 | private function __clone() { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /views/batch-list.php: -------------------------------------------------------------------------------- 1 |
    2 |

    3 |
    4 | 5 | prepare_items(); 8 | $list_table->display(); 9 | ?> 10 |
    11 |
    -------------------------------------------------------------------------------- /views/batch-manage.php: -------------------------------------------------------------------------------- 1 | get_batch( $id ); 4 | if ( is_null( $batch ) ) { 5 | echo 'Batch not found.'; 6 | 7 | return; 8 | } 9 | $percentage = $batch->get_percentage(); 10 | 11 | $items_count = $batch->get_items_count(); 12 | $processed_count = $batch->get_processed_count(); 13 | ?> 14 | 15 |

    title; ?>

    16 | 17 |
    18 | 0 ): ?> 19 | 20 |
    21 |
      22 |
    • Total:
    • 23 |
    • Processed: (%)
    • 26 |
    27 |
    28 | 0 ? 'width:' . $percentage . '%' : ''; 30 | ?> 31 |
    32 |
    33 |
    34 | is_finished()): ?> 35 | 36 | 37 |
    38 |
    39 | 40 |
    41 | is_finished() ): ?> 42 | 43 | 44 | 45 | 46 | 47 | 48 |
    49 | 50 |
    51 |

    52 |

    53 |
    54 | 55 | 56 | 57 |
    58 | 59 | 65 | 66 | 67 | 130 | -------------------------------------------------------------------------------- /views/batch-view.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 |
    6 | $id)); ?> 7 |
    8 |
    -------------------------------------------------------------------------------- /wp-batch-processing.php: -------------------------------------------------------------------------------- 1 |