├── CHANGELOG.md ├── logo.png ├── .scrutinizer.yml ├── languages ├── en.mo ├── locomotive.pot └── en.po ├── screenshot.gif ├── screenshot.png ├── .gitignore ├── templates └── dashboard.php ├── tests ├── bootstrap.php ├── test-loader.php ├── types │ ├── test-batch-users.php │ ├── test-batch-comments.php │ ├── test-batch-posts.php │ ├── test-batch-terms.php │ └── test-batch-sites.php ├── test-functions.php └── test-batch.php ├── phpunit.xml.dist ├── multisite.xml.dist ├── codecoverage.xml.dist ├── composer.json ├── ruleset.xml ├── .travis.yml ├── assets ├── src │ └── js │ │ ├── components │ │ ├── ModalReset.js │ │ ├── BatchPicker.js │ │ └── Modal.js │ │ └── batch.jsx └── main.css ├── package.json ├── includes ├── batches │ ├── class-batch-users.php │ ├── class-batch-posts.php │ ├── class-batch-sites.php │ ├── class-batch-comments.php │ └── class-batch-terms.php ├── functions.php └── abstracts │ └── abstract-batch.php ├── .eslintrc.js ├── README.md ├── bin └── install-wp-tests.sh ├── gulpfile.js ├── locomotive.php └── composer.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | #### Version 0.0.1 - 2016/00/00 3 | * initial release -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktivstudios/locomotive/HEAD/logo.png -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 900 4 | -------------------------------------------------------------------------------- /languages/en.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktivstudios/locomotive/HEAD/languages/en.mo -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktivstudios/locomotive/HEAD/screenshot.gif -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktivstudios/locomotive/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.map 3 | coverage/* 4 | build/ 5 | vendor/ 6 | coverage.clover 7 | -------------------------------------------------------------------------------- /templates/dashboard.php: -------------------------------------------------------------------------------- 1 | 9 |
10 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertNotNull( LOCO_VERSION ); 15 | $this->assertNotNull( LOCO_PLUGIN_DIR ); 16 | $this->assertNotNull( LOCO_PLUGIN_URL ); 17 | $this->assertNotNull( LOCO_PLUGIN_FILE ); 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | ./includes 17 | locomotive.php 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /multisite.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./includes 20 | locomotive.php 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /codecoverage.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | ./includes 17 | locomotive.php 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reaktivstudios/locomotive", 3 | "description": "Run batch processes in WordPress.", 4 | "license": "GPL2+", 5 | "type": "wordpress-plugin", 6 | "authors": [ 7 | { 8 | "name": "Reaktiv Studios", 9 | "homepage": "http://reaktivstudios.com/" 10 | } 11 | ], 12 | "require-dev": { 13 | "squizlabs/php_codesniffer": "3.*", 14 | "wp-coding-standards/wpcs": "2.3", 15 | "phpunit/phpunit": "~9.0" 16 | }, 17 | "scripts": { 18 | "post-install-cmd": "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs", 19 | "post-update-cmd" : "\"vendor/bin/phpcs\" --config-set installed_paths vendor/wp-coding-standards/wpcs" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sniffs for the coding standards of the locomotive plugin 4 | 5 | ./vendor/* 6 | ./tests/* 7 | ./node_modules/* 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | php: 8 | - '5.5' 9 | - '5.6' 10 | - '7.0' 11 | - '7.1' 12 | 13 | env: 14 | - WP_VERSION=latest WP_MULTISITE=0 15 | - WP_VERSION=latest WP_MULTISITE=1 16 | - WP_VERSION=4.6 WP_MULTISITE=0 17 | 18 | matrix: 19 | exclude: 20 | - php: '7.1' 21 | env: WP_VERSION=4.6 WP_MULTISITE=0 22 | 23 | before_script: 24 | - nvm install node 25 | - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 26 | - composer install 27 | - npm install -g gulp 28 | - npm install 29 | 30 | script: 31 | - gulp lint 32 | - phpunit -c codecoverage.xml.dist 33 | 34 | after_script: 35 | - wget https://scrutinizer-ci.com/ocular.phar 36 | - php ocular.phar code-coverage:upload --access-token="$SCRUTINIZER_TOKEN" --format=php-clover coverage.clover 37 | -------------------------------------------------------------------------------- /assets/src/js/components/ModalReset.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Modal component. 5 | */ 6 | var ModalReset = React.createClass( { 7 | /** 8 | * Type Checking 9 | * @type {Object} 10 | */ 11 | propTypes: { 12 | isOpen: React.PropTypes.bool, 13 | resetBatch: React.PropTypes.func, 14 | toggleResetModal: React.PropTypes.func 15 | }, 16 | 17 | mixins: [ 18 | require( 'react-onclickoutside' ) 19 | ], 20 | 21 | handleClickOutside : function () { 22 | if ( this.props.isOpen ) { 23 | this.props.toggleResetModal( false ); 24 | } 25 | }, 26 | 27 | closeModal: function () { 28 | this.props.toggleResetModal( false ); 29 | }, 30 | 31 | render : function () { 32 | var classes = 'locomotive-overlay'; 33 | 34 | if ( this.props.isOpen ) { 35 | classes += ' is-open'; 36 | } 37 | 38 | return ( 39 |
40 |
41 |

Are you sure you want to reset this process?

42 |

Doing so will reset the last run time back to never, and delete all associated post meta and options data.

43 | 44 | 45 | 46 |
47 |
48 | ); 49 | } 50 | } ); 51 | 52 | export default ModalReset; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "locomotive", 3 | "version": "0.1.0", 4 | "description": "Batch Processing WordPress plugin from Reaktiv Studios", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/reaktivstudios/locomotive.git" 12 | }, 13 | "author": "Reaktiv Studios, Zach Wills", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/reaktivstudios/locomotive/issues" 17 | }, 18 | "homepage": "https://github.com/reaktivstudios/locomotive#readme", 19 | "dependencies": { 20 | "babel-preset-es2015": "^6.1.2", 21 | "babel-preset-react": "^6.1.2", 22 | "babelify": "^7.2.0", 23 | "browserify": "^12.0.1", 24 | "chalk": "^1.1.1", 25 | "gulp": "^3.9.0", 26 | "gulp-duration": "0.0.0", 27 | "gulp-livereload": "^3.8.1", 28 | "gulp-notify": "^2.2.0", 29 | "gulp-rename": "^1.2.2", 30 | "gulp-sourcemaps": "^1.6.0", 31 | "react": "^0.14.2", 32 | "react-dom": "^0.14.2", 33 | "react-onclickoutside": "^4.5.0", 34 | "run-sequence": "^1.2.1", 35 | "utils-merge": "^1.0.0", 36 | "vinyl-buffer": "^1.0.0", 37 | "vinyl-source-stream": "^1.1.0", 38 | "watchify": "^3.6.0" 39 | }, 40 | "devDependencies": { 41 | "eslint": ">=4.18.2", 42 | "eslint-plugin-react": "^6.8.0", 43 | "eslintify": "^3.1.0", 44 | "gulp-eslint": "^3.0.1", 45 | "gulp-phpcs": "^1.1.1", 46 | "gulp-shell": "^0.5.2", 47 | "gulp-sort": "^2.0.0", 48 | "gulp-uglify": "^1.5.1", 49 | "gulp-util": "^3.0.7", 50 | "gulp-wp-pot": "^1.3.1", 51 | "minifyify": "^7.1.0", 52 | "phplint": "^1.7.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /languages/locomotive.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 locomotive 2 | # This file is distributed under the same license as the locomotive package. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: locomotive\n" 6 | "Report-Msgid-Bugs-To: https://github.com/reaktivstudios/locomotive/issues\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "PO-Revision-Date: 2016-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: Andrew Norcross \n" 12 | "Language-Team: Reaktiv Studios \n" 13 | "X-Poedit-Basepath: ..\n" 14 | "X-Poedit-SourceCharset: UTF-8\n" 15 | "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\n" 16 | "X-Poedit-SearchPath-0: .\n" 17 | "X-Poedit-SearchPathExcluded-0: *.js\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | #: includes/abstracts/abstract-batch.php:138 20 | msgid "Batch name must be defined." 21 | msgstr "" 22 | 23 | #: includes/abstracts/abstract-batch.php:150 24 | msgid "Batch type must be defined." 25 | msgstr "" 26 | 27 | #: includes/abstracts/abstract-batch.php:156 28 | msgid "An array of args must be defined." 29 | msgstr "" 30 | 31 | #: includes/abstracts/abstract-batch.php:162 32 | msgid "A callback must be defined." 33 | msgstr "" 34 | 35 | #: includes/abstracts/abstract-batch.php:198 36 | msgid "No results found." 37 | msgstr "" 38 | 39 | #: includes/abstracts/abstract-batch.php:300 40 | msgid "Failed" 41 | msgstr "" 42 | 43 | #: includes/functions.php:77 44 | msgid "ago" 45 | msgstr "" 46 | 47 | #: locomotive.php:162, locomotive.php:195 48 | msgid "Batch process not specified." 49 | msgstr "" 50 | 51 | #: locomotive.php:168 52 | msgid "Step must be defined." 53 | msgstr "" 54 | -------------------------------------------------------------------------------- /assets/src/js/components/BatchPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Potential Batch listings. 5 | */ 6 | var BatchPicker = React.createClass( { 7 | /** 8 | * Type Checking 9 | * @type {Object} 10 | */ 11 | propTypes: { 12 | batches: React.PropTypes.object, 13 | updateSelectedBatch: React.PropTypes.func, 14 | runBatch: React.PropTypes.func, 15 | canInteractWithBatch: React.PropTypes.bool, 16 | toggleResetModal: React.PropTypes.func 17 | }, 18 | /** 19 | * Render an individual batch option. 20 | * 21 | * @param key Used to get the right batch from this.props.batches. 22 | * @returns {JSX} 23 | */ 24 | renderBatchOption : function ( key ) { 25 | var batch = this.props.batches[ key ]; 26 | 27 | return ( 28 |
  • 29 | 30 | 33 |
  • 34 | ); 35 | }, 36 | 37 | /** 38 | * Render the batch processes list. 39 | * 40 | * @returns {JSX} 41 | */ 42 | render : function () { 43 | return ( 44 |
    45 |
      46 | { Object.keys( this.props.batches ).map( this.renderBatchOption ) } 47 |
    48 | 49 | 50 | 51 |
    52 | ); 53 | } 54 | } ); 55 | 56 | export default BatchPicker; 57 | -------------------------------------------------------------------------------- /includes/batches/class-batch-users.php: -------------------------------------------------------------------------------- 1 | 10, 31 | 'offset' => 0, 32 | ); 33 | 34 | /** 35 | * Get results function for the registered batch process. 36 | * 37 | * @return array \WP_User_query->get_results() result. 38 | */ 39 | public function batch_get_results() { 40 | $query = new WP_User_Query( $this->args ); 41 | $total_users = $query->get_total(); 42 | $this->set_total_num_results( $total_users ); 43 | return $query->get_results(); 44 | } 45 | 46 | /** 47 | * Clear the result status for a batch. 48 | * 49 | * @return bool 50 | */ 51 | public function batch_clear_result_status() { 52 | return delete_metadata( 'user', null, $this->slug . '_status', '', true ); 53 | } 54 | 55 | /** 56 | * Get the status of a result. 57 | * 58 | * @param \WP_User $result The result we want to get status of. 59 | */ 60 | public function get_result_item_status( $result ) { 61 | return get_user_meta( $result->data->ID, $this->slug . '_status', true ); 62 | } 63 | 64 | /** 65 | * Update the meta info on a result. 66 | * 67 | * @param \WP_User $result The result we want to track meta data on. 68 | * @param string $status Status of this result in the batch. 69 | */ 70 | public function update_result_item_status( $result, $status ) { 71 | return update_user_meta( $result->data->ID, $this->slug . '_status', $status ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /includes/batches/class-batch-posts.php: -------------------------------------------------------------------------------- 1 | 'post', 31 | 'posts_per_page' => 10, 32 | 'offset' => 0, 33 | ); 34 | 35 | /** 36 | * Get results function for the registered batch process. 37 | * 38 | * @return array \WP_Query->get_posts() result. 39 | */ 40 | public function batch_get_results() { 41 | $query = new WP_Query( $this->args ); 42 | $total_posts = $query->found_posts; 43 | $this->set_total_num_results( $total_posts ); 44 | return $query->get_posts(); 45 | } 46 | 47 | /** 48 | * Clear the result status for a batch. 49 | * 50 | * @return bool 51 | */ 52 | public function batch_clear_result_status() { 53 | return delete_post_meta_by_key( $this->slug . '_status' ); 54 | } 55 | 56 | /** 57 | * Get the status of a result. 58 | * 59 | * @param \WP_Post $result The result we want to get status of. 60 | */ 61 | public function get_result_item_status( $result ) { 62 | return get_post_meta( $result->ID, $this->slug . '_status', true ); 63 | } 64 | 65 | /** 66 | * Update the meta info on a result. 67 | * 68 | * @param \WP_Post $result The result we want to track meta data on. 69 | * @param string $status Status of this result in the batch. 70 | */ 71 | public function update_result_item_status( $result, $status ) { 72 | return update_post_meta( $result->ID, $this->slug . '_status', $status ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /includes/batches/class-batch-sites.php: -------------------------------------------------------------------------------- 1 | 10, 31 | 'offset' => 0, 32 | 'no_found_rows' => false, 33 | ); 34 | 35 | /** 36 | * Get results function for the registered batch process. 37 | * 38 | * @return array \WP_Site_Query->get_results() result. 39 | */ 40 | public function batch_get_results() { 41 | $query = new WP_Site_Query( $this->args ); 42 | $total_sites = $query->found_sites; 43 | $this->set_total_num_results( $total_sites ); 44 | return $query->get_sites(); 45 | } 46 | 47 | /** 48 | * Clear the result status for a batch. 49 | * 50 | * @return bool 51 | */ 52 | public function batch_clear_result_status() { 53 | return delete_metadata( 'site', null, $this->slug . '_status', '', true ); 54 | } 55 | 56 | /** 57 | * Get the status of a result. 58 | * 59 | * @param \WP_Site $result The result we want to get status of. 60 | */ 61 | public function get_result_item_status( $result ) { 62 | return get_metadata( 'site', $result->blog_id, $this->slug . '_status', true ); 63 | } 64 | 65 | /** 66 | * Update the meta info on a result. 67 | * 68 | * @param \WP_Site $result The result we want to track meta data on. 69 | * @param string $status Status of this result in the batch. 70 | */ 71 | public function update_result_item_status( $result, $status ) { 72 | return update_metadata( 'site', $result->blog_id, $this->slug . '_status', $status ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "experimentalObjectRestSpread": true, 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "react" 16 | ], 17 | "globals": { 18 | "jQuery": true, 19 | "require": true, 20 | "batch": true 21 | }, 22 | "rules": { 23 | "array-bracket-spacing": [ "error", "always" ], 24 | "brace-style" : [ "error", "1tbs" ], 25 | "camelcase": [ "warn", { "properties": "never" } ], 26 | "computed-property-spacing": [ "error", "always" ], 27 | "curly" : "error", 28 | "eol-last": "error", 29 | "eqeqeq": "error", 30 | "indent": [ 1, "tab" ], 31 | "keyword-spacing": "error", 32 | "linebreak-style": [ "error", "unix" ], 33 | "no-caller": "error", 34 | "no-eq-null": "error", 35 | "no-else-return": "warn", 36 | "no-mixed-spaces-and-tabs": [1, "smart-tabs"], 37 | "no-nested-ternary": "error", 38 | "no-shadow": "error", 39 | "no-spaced-func": "error", 40 | "no-trailing-spaces": "error", 41 | "no-unused-expressions": "error", 42 | "no-unused-vars": "error", 43 | "object-curly-spacing": [ "error", "always" ], 44 | "quotes": [ "error", "single" ], 45 | "react/no-direct-mutation-state" : "off", 46 | "react/jsx-curly-spacing": [ 1, "always" ], 47 | "react/jsx-space-before-closing" : "warn", 48 | "semi": [ "error", "always" ], 49 | "semi-spacing": "warn", 50 | "space-before-blocks" : "error", 51 | "space-before-function-paren" : "error", 52 | "space-in-parens" : [ "error", "always" ], 53 | "wrap-iife": [ "error", "any" ], 54 | "yoda": [ "warn", "always" ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /includes/batches/class-batch-comments.php: -------------------------------------------------------------------------------- 1 | 10, 31 | 'offset' => 0, 32 | 'no_found_rows' => false, 33 | ); 34 | 35 | /** 36 | * Get results function for the registered batch process. 37 | * 38 | * @return array \WP_Term_Query->get_results() result. 39 | */ 40 | public function batch_get_results() { 41 | $query = new WP_Comment_Query( $this->args ); 42 | $total_comments = $query->found_comments; 43 | $this->set_total_num_results( $total_comments ); 44 | return $query->get_comments(); 45 | } 46 | 47 | /** 48 | * Clear the result status for a batch. 49 | * 50 | * @return bool 51 | */ 52 | public function batch_clear_result_status() { 53 | return delete_metadata( 'comment', null, $this->slug . '_status', '', true ); 54 | } 55 | 56 | /** 57 | * Get the status of a result. 58 | * 59 | * @param \WP_Term $result The result we want to get status of. 60 | */ 61 | public function get_result_item_status( $result ) { 62 | return get_comment_meta( $result->comment_ID, $this->slug . '_status', true ); 63 | } 64 | 65 | /** 66 | * Update the meta info on a result. 67 | * 68 | * @param \WP_Term $result The result we want to track meta data on. 69 | * @param string $status Status of this result in the batch. 70 | */ 71 | public function update_result_item_status( $result, $status ) { 72 | return update_comment_meta( $result->comment_ID, $this->slug . '_status', $status ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /languages/en.po: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 locomotive 2 | # This file is distributed under the same license as the locomotive package. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: locomotive\n" 6 | "Report-Msgid-Bugs-To: https://github.com/reaktivstudios/locomotive/issues\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=UTF-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | "PO-Revision-Date: 2016-09-25 13:08-0400\n" 11 | "Language-Team: Reaktiv Studios \n" 12 | "X-Poedit-Basepath: ..\n" 13 | "X-Poedit-SourceCharset: UTF-8\n" 14 | "X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c;_n_noop:1,2;_nx_noop:3c,1,2;__ngettext_noop:1,2\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | "POT-Creation-Date: \n" 17 | "X-Generator: Poedit 1.8.9\n" 18 | "Last-Translator: \n" 19 | "Language: en\n" 20 | "X-Poedit-SearchPath-0: .\n" 21 | "X-Poedit-SearchPathExcluded-0: *.js\n" 22 | 23 | #: includes/abstracts/abstract-batch.php:138 24 | msgid "Batch name must be defined." 25 | msgstr "Batch name must be defined." 26 | 27 | #: includes/abstracts/abstract-batch.php:150 28 | msgid "Batch type must be defined." 29 | msgstr "Batch type must be defined." 30 | 31 | #: includes/abstracts/abstract-batch.php:156 32 | msgid "An array of args must be defined." 33 | msgstr "An array of args must be defined." 34 | 35 | #: includes/abstracts/abstract-batch.php:162 36 | msgid "A callback must be defined." 37 | msgstr "A callback must be defined." 38 | 39 | #: includes/abstracts/abstract-batch.php:198 40 | msgid "No results found." 41 | msgstr "No results found." 42 | 43 | #: includes/abstracts/abstract-batch.php:300 44 | msgid "Failed" 45 | msgstr "Failed" 46 | 47 | #: includes/functions.php:77 48 | msgid "ago" 49 | msgstr "ago" 50 | 51 | #: locomotive.php:162, locomotive.php:195 52 | msgid "Batch process not specified." 53 | msgstr "Batch process not specified." 54 | 55 | #: locomotive.php:168 56 | msgid "Step must be defined." 57 | msgstr "Step must be defined." 58 | -------------------------------------------------------------------------------- /includes/batches/class-batch-terms.php: -------------------------------------------------------------------------------- 1 | 10, 31 | 'offset' => 0, 32 | ); 33 | 34 | /** 35 | * Get results function for the registered batch process. 36 | * 37 | * @return array \WP_Term_Query->get_results() result. 38 | */ 39 | public function batch_get_results() { 40 | $query = new WP_Term_Query( $this->args ); 41 | // Need to do a count query in order to get all possible terms as an integer. 42 | $count_args = wp_parse_args( array( 'fields' => 'count', 'offset' => 0 ), $this->args ); 43 | $count = new WP_Term_Query( $count_args ); 44 | $this->set_total_num_results( $count->get_terms() ); 45 | return $query->get_terms(); 46 | } 47 | 48 | /** 49 | * Clear the result status for a batch. 50 | * 51 | * @return bool 52 | */ 53 | public function batch_clear_result_status() { 54 | return delete_metadata( 'term', null, $this->slug . '_status', '', true ); 55 | } 56 | 57 | /** 58 | * Get the status of a result. 59 | * 60 | * @param \WP_Term $result The result we want to get status of. 61 | */ 62 | public function get_result_item_status( $result ) { 63 | return get_term_meta( $result->data->term_id, $this->slug . '_status', true ); 64 | } 65 | 66 | /** 67 | * Update the meta info on a result. 68 | * 69 | * @param \WP_Term $result The result we want to track meta data on. 70 | * @param string $status Status of this result in the batch. 71 | */ 72 | public function update_result_item_status( $result, $status ) { 73 | return update_term_meta( $result->data->term_id, $this->slug . '_status', $status ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Locomotive 2 | ========== 3 | 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/reaktivstudios/locomotive/badges/quality-score.png?b=master&s=86399ae1ed8459dbcaa0c4a5d5e34947d7454cf8)](https://scrutinizer-ci.com/g/reaktivstudios/locomotive/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/reaktivstudios/locomotive/badges/coverage.png?b=master&s=656ebaea7636b3882b1834f7226c53327e826bb2)](https://scrutinizer-ci.com/g/reaktivstudios/locomotive/?branch=master) 5 | 6 | ## About 7 | ![Locomotive Logo](logo.png?raw=true "Locomotive Logo") 8 | 9 | Creating batch processes in WordPress has never been so easy. If you've ever wanted to query a large dataset and perform simple and repeatable actions, then Locomotive is for you. 10 | 11 | Locomotive allows developers to write a single function (or set of functions) to process actions across a large data set. These registered batch processes can be run with the click of a button from the WP admin as needed. Locomotive handles the complexity of batch processing by automatically chunking up data, checking for records that have already been processed and logging errors as they come in. 12 | 13 | ## Links 14 | * [Documentation](https://github.com/reaktivstudios/locomotive/wiki) 15 | * [Examples](https://github.com/reaktivstudios/locomotive/wiki/Examples) 16 | 17 | ## Quick Start Example 18 | 19 | #### Register a standard post query 20 | ``` php 21 | function my_post_query_batch_process() { 22 | 23 | register_batch_process( array( 24 | 'name' => 'Just another batch', 25 | 'type' => 'post', 26 | 'callback' => 'my_callback_function', 27 | 'args' => array( 28 | 'posts_per_page' => 1, 29 | 'post_type' => 'post', 30 | ), 31 | ) ); 32 | } 33 | add_action( 'locomotive_init', 'my_post_query_batch_process' ); 34 | ``` 35 | 36 | #### Hook In Callback Function 37 | ``` php 38 | /** 39 | * This is what we want to do with each individual result during a batch routine. 40 | * 41 | * @param array $result Individual result from batch query. 42 | */ 43 | function my_callback_function( $result ) { 44 | error_log( print_r( $result->post_title, true ) ); 45 | } 46 | ``` 47 | 48 | #### Start Batch Process 49 | ![Locomotive Menu](screenshot.gif?raw=true "Locomotive Menu") 50 | 51 | Navigate to Tools->Batches in the admin, select your batch, and click _Run_. 52 | 53 | ## Contributors 54 | * [Josh Eaton](https://github.com/jjeaton) 55 | * [Zach Wills](https://github.com/zachwills) 56 | * [Andrew Norcross](https://github.com/norcross) 57 | * [Jay Hoffmann](https://github.com/JasonHoffmann) 58 | 59 | ## Changelog 60 | 61 | See [CHANGELOG.md](CHANGELOG.md). 62 | 63 | 64 | #### [Pull requests](https://github.com/reaktivstudios/locomotive/pulls) are very much welcome and encouraged. 65 | -------------------------------------------------------------------------------- /includes/functions.php: -------------------------------------------------------------------------------- 1 | register( $args ); 30 | break; 31 | 32 | case 'user': 33 | $batch_process = new Users(); 34 | $batch_process->register( $args ); 35 | break; 36 | case 'site': 37 | if ( is_multisite() ) { 38 | $batch_process = new Sites(); 39 | $batch_process->register( $args ); 40 | } 41 | break; 42 | 43 | case 'term': 44 | $batch_process = new Terms(); 45 | $batch_process->register( $args ); 46 | break; 47 | 48 | case 'comment': 49 | $batch_process = new Comments(); 50 | $batch_process->register( $args ); 51 | break; 52 | } 53 | } 54 | 55 | /** 56 | * Get the batch hooks that have been added and some info about them. 57 | * 58 | * @return array 59 | */ 60 | function locomotive_get_all_batches() { 61 | $batches = get_option( 'loco_batches', array() ); 62 | 63 | foreach ( $batches as $k => $batch ) { 64 | if ( $batch_status = get_option( 'loco_batch_' . $k ) ) { 65 | $last_run = locomotive_time_ago( $batch_status['timestamp'] ); 66 | $status = $batch_status['status']; 67 | } else { 68 | $last_run = 'never'; 69 | $status = 'new'; 70 | } 71 | 72 | $batches[ $k ]['last_run'] = $last_run; 73 | $batches[ $k ]['status'] = $status; 74 | } 75 | 76 | return $batches; 77 | } 78 | 79 | /** 80 | * Update the registered batches. 81 | * 82 | * @param array $batches Batches you want to register. 83 | */ 84 | function locomotive_update_registered_batches( $batches ) { 85 | return update_option( 'loco_batches', $batches ); 86 | } 87 | 88 | /** 89 | * Template function for showing time ago. 90 | * 91 | * @todo Move this to a template functions file. 92 | * 93 | * @param integer $time Timestamp. 94 | */ 95 | function locomotive_time_ago( $time ) { 96 | return sprintf( _x( '%s ago', 'amount of time that has passed', 'locomotive' ), human_time_diff( $time, current_time( 'timestamp' ) ) ); 97 | } 98 | 99 | /** 100 | * Clear all existing batches. 101 | */ 102 | function locomotive_clear_existing_batches() { 103 | return update_option( 'loco_batches', array() ); 104 | } 105 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Styles for the Locomotive plugin. 3 | */ 4 | 5 | #batch-main * { 6 | box-sizing: border-box; 7 | } 8 | 9 | .batch-processes small { 10 | text-transform: lowercase; 11 | } 12 | 13 | .locomotive-form #submit { 14 | margin-right: 12px; 15 | } 16 | 17 | .batch-processes { 18 | margin: 10px 0 20px; 19 | } 20 | 21 | .batch-processes li { 22 | margin-bottom: 10px; 23 | } 24 | 25 | .batch-processes li input[type="radio"] { 26 | margin-top: 0; 27 | } 28 | 29 | .batch-picker .button-link { 30 | background: none; 31 | border: none; 32 | box-shadow: none; 33 | margin-left: 20px; 34 | font-size: 12px; 35 | color: #cc7373; 36 | line-height: 26px; 37 | } 38 | 39 | .batch-picker .button-link:active { 40 | box-shadow: none; 41 | transform: none; 42 | color: #666; 43 | } 44 | 45 | .batch-picker .button-link[disabled] { 46 | background: none; 47 | color: #ccc; 48 | } 49 | /** 50 | * Overlay that contains info about current batch process. 51 | */ 52 | .locomotive-overlay { 53 | transition: all .5s ease-in-out; 54 | -webkit-transition: all .5s ease-in-out; 55 | opacity: 0; 56 | display: table; 57 | 58 | padding: 48px 24px; 59 | width: 75%; 60 | 61 | margin: auto; 62 | position: absolute; 63 | top: -650px; 64 | left: 0; right: 0; 65 | 66 | background: white; 67 | box-shadow: 0 0 10px #ccc; 68 | } 69 | .batch-overlay__inner { 70 | display: table-cell; 71 | vertical-align: middle; 72 | } 73 | .locomotive-overlay.is-open { 74 | top: 0; 75 | opacity: 1; 76 | } 77 | .locomotive-overlay .close { 78 | position: absolute; 79 | top: 8px; 80 | right: 12px; 81 | cursor: pointer; 82 | } 83 | .locomotive-overlay h2 { 84 | margin-top: 0; 85 | margin-bottom: 12px; 86 | } 87 | .batch-info { 88 | margin-bottom: 6px; 89 | } 90 | .progress-bar { 91 | background: #f0f0f0; 92 | color: white; 93 | line-height: 35px; 94 | padding-left: 12px; 95 | position: relative; 96 | } 97 | .progress-bar__text { 98 | display: block; 99 | position: relative; 100 | z-index: 10; 101 | } 102 | .progress-bar__visual { 103 | background: #27ae60; 104 | height: 100%; 105 | position: absolute; 106 | top: 0; 107 | left: 0; 108 | z-index: 5; 109 | } 110 | 111 | .batch-success__title { 112 | color: #27ae60; 113 | } 114 | 115 | .batch-success__title:before { 116 | margin-right: 5px; 117 | } 118 | 119 | .batch-error .red { 120 | color: #e74c3c; 121 | } 122 | .batch-error .green { 123 | color: #2ecc71; 124 | } 125 | .batch-error__title { 126 | color: #e74c3c; 127 | } 128 | .batch-error__title:before { 129 | margin-right: 5px; 130 | } 131 | .batch-error__list { 132 | max-height: 250px; 133 | overflow: scroll; 134 | background: #eee; 135 | padding: 1em; 136 | font-family: monospace; 137 | } 138 | .batch-error__btn { 139 | background: none; 140 | border: none; 141 | font-size: 12px; 142 | font-weight: bold; 143 | cursor: pointer; 144 | } 145 | .batch-error__btn:before { 146 | width: 15px; 147 | height: 15px; 148 | line-height: 16px; 149 | margin-right: 3px; 150 | } 151 | -------------------------------------------------------------------------------- /assets/src/js/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Modal component. 5 | */ 6 | var Modal = React.createClass( { 7 | /** 8 | * Type Checking 9 | * @type {Object} 10 | */ 11 | propTypes: { 12 | isOpen: React.PropTypes.bool, 13 | toggleProcessing: React.PropTypes.func, 14 | batchInfo: React.PropTypes.object, 15 | batchErrors: React.PropTypes.array, 16 | selectedBatch: React.PropTypes.string 17 | }, 18 | 19 | getInitialState: function () { 20 | return { 21 | showErrors: false 22 | }; 23 | }, 24 | 25 | mixins: [ 26 | require( 'react-onclickoutside' ) 27 | ], 28 | 29 | handleClickOutside : function () { 30 | if ( this.props.isOpen ) { 31 | this.props.toggleProcessing( false ); 32 | } 33 | }, 34 | 35 | onErrorClick: function () { 36 | this.setState( { showErrors: !this.state.showErrors } ); 37 | }, 38 | 39 | render : function () { 40 | var classes = 'locomotive-overlay', 41 | batchInfo = this.props.batchInfo, 42 | errorClick = this.onErrorClick, 43 | batchTitle = ( batchInfo.batch_title ) ? batchInfo.batch_title : this.props.selectedBatch, 44 | batchErrors = this.props.batchErrors, 45 | numErrors = batchErrors.length, 46 | totalResults = parseInt( batchInfo.total_num_results ), 47 | numSuccess = ( ( totalResults / batchInfo.total_steps ) * batchInfo.current_step ) - numErrors, 48 | status = ( batchInfo.status ) ? batchInfo.status : '', 49 | progressStyle = { width: batchInfo.progress + '%' }, 50 | showErrors = this.state.showErrors; 51 | 52 | if ( this.props.isOpen ) { 53 | classes += ' is-open'; 54 | } 55 | 56 | /** 57 | * Return the title for the modal. 58 | * 59 | * @returns {JSX} 60 | */ 61 | var title = function () { 62 | if ( status ) { 63 | return

    { batchTitle }: { status }

    ; 64 | } 65 | 66 | return

    { batchTitle }

    ; 67 | }; 68 | 69 | var errorItems = batchErrors.map( function ( item ) { 70 | return ( 71 |
  • { item.item } failed with the message: { item.message }
  • 72 | ); 73 | } ); 74 | 75 | var errorList = function () { 76 | if ( showErrors ) { 77 | return ( 78 |
      79 | {errorItems} 80 |
    81 | ); 82 | } 83 | }; 84 | 85 | var infoBlock = function () { 86 | if ( batchInfo.error ) { 87 | return ( 88 |
    89 |

    Status: Failed

    90 |

    There were { numErrors } failed items and { numSuccess } successful items in your batch.

    91 | 97 | { errorList() } 98 |
    99 | ); 100 | } 101 | 102 | return ( 103 |
    104 |

    Status: Success!

    105 |
    106 | ); 107 | }; 108 | 109 | /** 110 | * Return content for the modal. 111 | * 112 | * @returns {JSX} 113 | */ 114 | var content = function () { 115 | return ( 116 |
    117 |
    118 | Progress: { batchInfo.progress }% 119 |
    120 |
    121 | { infoBlock() } 122 |
    123 | ); 124 | }; 125 | 126 | return ( 127 |
    128 |
    close
    129 |
    130 | { title() } 131 | { content() } 132 |
    133 |
    134 | ); 135 | } 136 | } ); 137 | 138 | export default Modal; 139 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # if [ $# -lt 3 ]; then 4 | # echo "usage: $0 [db-host] [wp-version]" 5 | # exit 1 6 | # fi 7 | 8 | DB_NAME=${1-wordpress_test} 9 | DB_USER=${2-root} 10 | DB_PASS=${3-root} 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 16 | 17 | download() { 18 | if [ `which curl` ]; then 19 | curl -s "$1" > "$2"; 20 | elif [ `which wget` ]; then 21 | wget -nv -O "$2" "$1" 22 | fi 23 | } 24 | 25 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 26 | WP_TESTS_TAG="tags/$WP_VERSION" 27 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 28 | WP_TESTS_TAG="trunk" 29 | else 30 | # http serves a single offer, whereas https serves multiple. we only want one 31 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 32 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 33 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 34 | if [[ -z "$LATEST_VERSION" ]]; then 35 | echo "Latest WordPress version could not be found" 36 | exit 1 37 | fi 38 | WP_TESTS_TAG="tags/$LATEST_VERSION" 39 | fi 40 | 41 | set -ex 42 | 43 | install_wp() { 44 | 45 | if [ -d $WP_CORE_DIR ]; then 46 | return; 47 | fi 48 | 49 | mkdir -p $WP_CORE_DIR 50 | 51 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 52 | mkdir -p /tmp/wordpress-nightly 53 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 54 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 55 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 56 | else 57 | if [ $WP_VERSION == 'latest' ]; then 58 | local ARCHIVE_NAME='latest' 59 | else 60 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 61 | fi 62 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 63 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 64 | fi 65 | 66 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 67 | } 68 | 69 | install_test_suite() { 70 | # portable in-place argument for both GNU sed and Mac OSX sed 71 | if [[ $(uname -s) == 'Darwin' ]]; then 72 | local ioption='-i .bak' 73 | else 74 | local ioption='-i' 75 | fi 76 | 77 | # set up testing suite if it doesn't yet exist 78 | if [ ! -d $WP_TESTS_DIR ]; then 79 | # set up testing suite 80 | mkdir -p $WP_TESTS_DIR 81 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 83 | fi 84 | 85 | if [ ! -f wp-tests-config.php ]; then 86 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 87 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php 88 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 89 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 90 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 92 | fi 93 | 94 | } 95 | 96 | install_db() { 97 | # parse DB_HOST for port or socket references 98 | local PARTS=(${DB_HOST//\:/ }) 99 | local DB_HOSTNAME=${PARTS[0]}; 100 | local DB_SOCK_OR_PORT=${PARTS[1]}; 101 | local EXTRA="" 102 | 103 | if ! [ -z $DB_HOSTNAME ] ; then 104 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 105 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 106 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 107 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 108 | elif ! [ -z $DB_HOSTNAME ] ; then 109 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 110 | fi 111 | fi 112 | 113 | # create database 114 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 115 | } 116 | 117 | install_wp 118 | install_test_suite 119 | install_db 120 | -------------------------------------------------------------------------------- /tests/types/test-batch-users.php: -------------------------------------------------------------------------------- 1 | users = $this->factory->user->create_many( 9 ); 23 | } 24 | 25 | 26 | /** 27 | * Test that user batch gets run. 28 | */ 29 | public function test_user_finished_run() { 30 | $user_batch = new Users(); 31 | 32 | $user_batch->register( array( 33 | 'name' => 'Hey there', 34 | 'type' => 'user', 35 | 'callback' => __NAMESPACE__ . '\\my_user_callback_function_test', 36 | 'args' => array( 37 | 'number' => 10, 38 | ), 39 | ) ); 40 | 41 | $batch_status = get_option( 'loco_batch_' . $user_batch->slug ); 42 | $this->assertFalse( $batch_status ); 43 | 44 | $user_batch->run( 1 ); 45 | 46 | $batch_status = get_option( 'loco_batch_' . $user_batch->slug ); 47 | $this->assertEquals( 'finished', $batch_status['status'] ); 48 | 49 | // Loop through each post and make sure our value was set. 50 | foreach ( $this->users as $user ) { 51 | $meta = get_user_meta( $user, 'custom-key', true ); 52 | $this->assertEquals( 'my-value', $meta ); 53 | 54 | $status = get_user_meta( $user, $user_batch->slug . '_status', true ); 55 | $this->assertEquals( 'success', $status ); 56 | } 57 | 58 | // Run again so it skips some. 59 | $user_batch->run( 1 ); 60 | } 61 | 62 | /** 63 | * Test that you can clear individual result status. 64 | */ 65 | public function test_clear_user_result_status() { 66 | $user_batch = new Users(); 67 | 68 | $user_batch->register( array( 69 | 'name' => 'Hey there', 70 | 'type' => 'user', 71 | 'callback' => __NAMESPACE__ . '\\my_user_callback_function_test', 72 | 'args' => array( 73 | 'number' => 10, 74 | ), 75 | ) ); 76 | 77 | $user_batch->run( 1 ); 78 | 79 | $user_batch->clear_result_status(); 80 | 81 | // Loop through each post and make sure our value was set. 82 | foreach ( $this->users as $user ) { 83 | $meta = get_user_meta( $user, 'custom-key', true ); 84 | $this->assertEquals( 'my-value', $meta ); 85 | 86 | $status = get_user_meta( $user, $user_batch->slug . '_status', true ); 87 | $this->assertEquals( '', $status ); 88 | } 89 | 90 | $batches = locomotive_get_all_batches(); 91 | $this->assertEquals( 'reset', $batches['hey-there']['status'] ); 92 | } 93 | 94 | /** 95 | * Test that users offset batch gets run. 96 | */ 97 | public function test_users_offset_run() { 98 | $user_batch = new Users(); 99 | 100 | $user_batch->register( array( 101 | 'name' => 'Hey there', 102 | 'type' => 'user', 103 | 'callback' => __NAMESPACE__ . '\\my_user_callback_function_test', 104 | 'args' => array( 105 | 'number' => 5, 106 | 'offset' => 5, 107 | ), 108 | ) ); 109 | 110 | $batch_status = get_option( 'loco_batch_' . $user_batch->slug ); 111 | $this->assertFalse( $batch_status ); 112 | 113 | $user_batch->run( 2 ); 114 | 115 | $batch_status = get_option( 'loco_batch_' . $user_batch->slug ); 116 | $this->assertEquals( 'finished', $batch_status['status'] ); 117 | } 118 | 119 | public function test_run_with_destructive_callback() { 120 | 121 | $user_batch = new Users(); 122 | 123 | $user_batch->register( array( 124 | 'name' => 'Hey there', 125 | 'type' => 'user', 126 | 'callback' => __NAMESPACE__ . '\\my_callback_delete_user', 127 | 'args' => array( 128 | 'number' => 5, 129 | ), 130 | ) ); 131 | 132 | // Simulate running twice with pass back of results number from client. 133 | $user_batch->run( 1 ); 134 | $_POST['total_num_results'] = 9; 135 | $user_batch->run( 2 ); 136 | 137 | // Check that all users have been deleted. There should be one remaining, because of the main User ID. 138 | $all_users = get_users(); 139 | $this->assertCount( 1, $all_users ); 140 | 141 | // Ensure that we are still getting a finished message. 142 | $batch_status = get_option( 'loco_batch_' . $user_batch->slug ); 143 | $this->assertEquals( 'finished', $batch_status['status'] ); 144 | } 145 | 146 | } 147 | 148 | /** 149 | * My callback function test for users. 150 | * 151 | * @param WP_User $result Result item. 152 | */ 153 | function my_user_callback_function_test( $result ) { 154 | update_metadata( 'user', $result->ID, 'custom-key', 'my-value' ); 155 | } 156 | 157 | /** 158 | * Callback function with destructive action (deletion). 159 | * 160 | * @param WP_User $result Result item. 161 | */ 162 | function my_callback_delete_user( $result ) { 163 | wp_delete_user( $result->ID ); 164 | } 165 | -------------------------------------------------------------------------------- /tests/types/test-batch-comments.php: -------------------------------------------------------------------------------- 1 | comments = $this->factory->comment->create_many( 10 ); 23 | $this->user = $this->factory->user->create(); 24 | } 25 | 26 | 27 | /** 28 | * Test that comment batch runs 29 | */ 30 | public function test_comments_finished_run() { 31 | $comment_batch = new Comments(); 32 | 33 | $comment_batch->register( array( 34 | 'name' => 'Hey there', 35 | 'type' => 'comment', 36 | 'callback' => __NAMESPACE__ . '\\my_comment_callback_function', 37 | 'args' => array( 38 | 'number' => 10, 39 | ), 40 | ) ); 41 | 42 | $batch_status = get_option( 'loco_batch_' . $comment_batch->slug ); 43 | $this->assertFalse( $batch_status ); 44 | 45 | $comment_batch->run( 1 ); 46 | 47 | $batch_status = get_option( 'loco_batch_' . $comment_batch->slug ); 48 | $this->assertEquals( 'finished', $batch_status['status'] ); 49 | 50 | // Loop through each post and make sure our value was set. 51 | foreach ( $this->comments as $comment ) { 52 | $meta = get_metadata( 'comment', $comment, 'custom-key', true ); 53 | $this->assertEquals( 'my-value', $meta ); 54 | 55 | $status = get_metadata( 'comment', $comment, $comment_batch->slug . '_status', true ); 56 | $this->assertEquals( 'success', $status ); 57 | } 58 | 59 | // Run again so it skips some. 60 | $comment_batch->run( 1 ); 61 | } 62 | 63 | /** 64 | * Test that you can clear individual result status. 65 | */ 66 | public function test_clear_comments_result_status() { 67 | $comment_batch = new Comments(); 68 | 69 | $comment_batch->register( array( 70 | 'name' => 'Hey there', 71 | 'type' => 'comment', 72 | 'callback' => __NAMESPACE__ . '\\my_comment_callback_function', 73 | 'args' => array( 74 | 'number' => 10, 75 | ), 76 | ) ); 77 | 78 | $comment_batch->run( 1 ); 79 | 80 | $comment_batch->clear_result_status(); 81 | 82 | // Loop through each post and make sure our value was set. 83 | foreach ( $this->comments as $comment ) { 84 | $meta = get_metadata( 'comment', $comment, 'custom-key', true ); 85 | $this->assertEquals( 'my-value', $meta ); 86 | 87 | $status = get_metadata( 'comment', $comment, $comment_batch->slug . '_status', true ); 88 | $this->assertEquals( '', $status ); 89 | } 90 | 91 | $batches = locomotive_get_all_batches(); 92 | $this->assertEquals( 'reset', $batches['hey-there']['status'] ); 93 | } 94 | 95 | /** 96 | * Test that comments offset batch gets run. 97 | */ 98 | public function test_comments_offset_run() { 99 | $comment_batch = new Comments(); 100 | 101 | $comment_batch->register( array( 102 | 'name' => 'Hey there', 103 | 'type' => 'comment', 104 | 'callback' => __NAMESPACE__ . '\\my_comment_callback_function', 105 | 'args' => array( 106 | 'number' => 5, 107 | 'offset' => 5, 108 | ), 109 | ) ); 110 | 111 | $batch_status = get_option( 'loco_batch_' . $comment_batch->slug ); 112 | $this->assertFalse( $batch_status ); 113 | 114 | $comment_batch->run( 2 ); 115 | 116 | $batch_status = get_option( 'loco_batch_' . $comment_batch->slug ); 117 | $this->assertEquals( 'finished', $batch_status['status'] ); 118 | } 119 | 120 | /** 121 | * Test that batch gets run when data is destroyed during process. 122 | */ 123 | public function test_run_with_destructive_callback() { 124 | 125 | $comment_batch = new Comments(); 126 | 127 | $comment_batch->register( array( 128 | 'name' => 'Hey there', 129 | 'type' => 'comment', 130 | 'callback' => __NAMESPACE__ . '\\my_callback_delete_comment', 131 | 'args' => array( 132 | 'number' => 5, 133 | ), 134 | ) ); 135 | 136 | // Simulate running twice with pass back of results number from client. 137 | $comment_batch->run( 1 ); 138 | $_POST['total_num_results'] = 10; 139 | $comment_batch->run( 2 ); 140 | 141 | // Check that all comments have been deleted. 142 | $all_posts = get_comments(); 143 | $this->assertCount( 0, $all_posts ); 144 | 145 | // Ensure that we are still getting a finished message. 146 | $batch_status = get_option( 'loco_batch_' . $comment_batch->slug ); 147 | $this->assertEquals( 'finished', $batch_status['status'] ); 148 | } 149 | 150 | } 151 | 152 | function my_comment_callback_function( $result ) { 153 | update_comment_meta( $result->comment_ID, 'custom-key', 'my-value' ); 154 | } 155 | 156 | /** 157 | * Callback function with destructive action (deletion). 158 | * 159 | * @param WP_Comment $result Result item. 160 | */ 161 | function my_callback_delete_comment( $result ) { 162 | wp_delete_comment( $result->comment_ID, true ); 163 | } 164 | -------------------------------------------------------------------------------- /tests/types/test-batch-posts.php: -------------------------------------------------------------------------------- 1 | posts = $this->factory->post->create_many( 10 ); 23 | } 24 | 25 | 26 | /** 27 | * Test that post batch gets run. 28 | */ 29 | public function test_post_finished_run() { 30 | 31 | $post_batch = new Posts(); 32 | 33 | $post_batch->register( array( 34 | 'name' => 'Hey there', 35 | 'type' => 'post', 36 | 'callback' => __NAMESPACE__ . '\\my_post_callback_function_test', 37 | 'args' => array( 38 | 'posts_per_page' => 10, 39 | 'post_type' => 'post', 40 | ), 41 | ) ); 42 | 43 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 44 | 45 | $this->assertFalse( $batch_status ); 46 | 47 | $post_batch->run( 1 ); 48 | 49 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 50 | $this->assertEquals( 'finished', $batch_status['status'] ); 51 | 52 | // Loop through each post and make sure our value was set. 53 | foreach ( $this->posts as $post ) { 54 | $meta = get_post_meta( $post, 'custom-key', true ); 55 | $this->assertEquals( 'my-value', $meta ); 56 | 57 | $status = get_post_meta( $post, $post_batch->slug . '_status', true ); 58 | $this->assertEquals( 'success', $status ); 59 | } 60 | 61 | // Run again so it skips some. 62 | $post_batch->run( 1 ); 63 | } 64 | 65 | /** 66 | * Test that you can clear individual result status. 67 | */ 68 | public function test_clear_result_status() { 69 | 70 | $post_batch = new Posts(); 71 | 72 | $post_batch->register( array( 73 | 'name' => 'Hey there', 74 | 'type' => 'post', 75 | 'callback' => __NAMESPACE__ . '\\my_post_callback_function_test', 76 | 'args' => array( 77 | 'posts_per_page' => 10, 78 | 'post_type' => 'post', 79 | ), 80 | ) ); 81 | 82 | $post_batch->run( 1 ); 83 | 84 | $post_batch->clear_result_status(); 85 | 86 | // Loop through each post and make sure our value was set. 87 | foreach ( $this->posts as $post ) { 88 | $meta = get_post_meta( $post, 'custom-key', true ); 89 | $this->assertEquals( 'my-value', $meta ); 90 | 91 | $status = get_post_meta( $post, $post_batch->slug . '_status', true ); 92 | $this->assertEquals( '', $status ); 93 | } 94 | 95 | $batches = locomotive_get_all_batches(); 96 | $this->assertEquals( 'reset', $batches['hey-there']['status'] ); 97 | } 98 | 99 | /** 100 | * Test that batch gets run. 101 | */ 102 | public function test_offset_run() { 103 | 104 | $post_batch = new Posts(); 105 | 106 | $post_batch->register( array( 107 | 'name' => 'Hey there', 108 | 'type' => 'post', 109 | 'callback' => __NAMESPACE__ . '\\my_post_callback_function_test', 110 | 'args' => array( 111 | 'posts_per_page' => 5, 112 | 'post_type' => 'post', 113 | ), 114 | ) ); 115 | 116 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 117 | $this->assertFalse( $batch_status ); 118 | 119 | $post_batch->run( 2 ); 120 | 121 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 122 | $this->assertEquals( 'finished', $batch_status['status'] ); 123 | } 124 | 125 | /** 126 | * Test that batch gets run when data is destroyed during process. 127 | */ 128 | public function test_run_with_destructive_callback() { 129 | 130 | $post_batch = new Posts(); 131 | 132 | $post_batch->register( array( 133 | 'name' => 'Hey there', 134 | 'type' => 'post', 135 | 'callback' => __NAMESPACE__ . '\\my_callback_delete_post', 136 | 'args' => array( 137 | 'posts_per_page' => 5, 138 | 'post_type' => 'post', 139 | ), 140 | ) ); 141 | 142 | // Simulate running twice with pass back of results number from client. 143 | $post_batch->run( 1 ); 144 | $_POST['total_num_results'] = 10; 145 | $post_batch->run( 2 ); 146 | 147 | // Check that all posts have been deleted. 148 | $all_posts = get_posts( array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1 ) ); 149 | $this->assertCount( 0, $all_posts ); 150 | 151 | // Ensure that we are still getting a finished message. 152 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 153 | $this->assertEquals( 'finished', $batch_status['status'] ); 154 | } 155 | 156 | } 157 | 158 | /** 159 | * My callback function test for posts. 160 | * 161 | * @param WP_Post $result Result item. 162 | */ 163 | function my_post_callback_function_test( $result ) { 164 | update_post_meta( $result->ID, 'custom-key', 'my-value' ); 165 | } 166 | 167 | /** 168 | * Callback function with destructive action (deletion). 169 | * 170 | * @param WP_Post $result Result item. 171 | */ 172 | function my_callback_delete_post( $result ) { 173 | wp_delete_post( $result->ID, true ); 174 | } 175 | -------------------------------------------------------------------------------- /tests/types/test-batch-terms.php: -------------------------------------------------------------------------------- 1 | terms = $this->factory->category->create_many( 5 ); 23 | $this->tags = $this->factory->tag->create_many( 5 ); 24 | } 25 | 26 | 27 | /** 28 | * Test that term batch gets run. 29 | */ 30 | public function test_term_finished_run() { 31 | 32 | $term_batch = new Terms(); 33 | 34 | $term_batch->register( array( 35 | 'name' => 'Hey there', 36 | 'type' => 'term', 37 | 'callback' => __NAMESPACE__ . '\\my_term_callback_function_test', 38 | 'args' => array( 39 | 'number' => 10, 40 | 'taxonomy' => 'category', 41 | 'hide_empty' => false, 42 | ), 43 | ) ); 44 | 45 | $batch_status = get_option( 'loco_batch_' . $term_batch->slug ); 46 | $this->assertFalse( $batch_status ); 47 | 48 | $term_batch->run( 1 ); 49 | 50 | $batch_status = get_option( 'loco_batch_' . $term_batch->slug ); 51 | $this->assertEquals( 'finished', $batch_status['status'] ); 52 | 53 | // Loop through each post and make sure our value was set. 54 | foreach ( $this->terms as $term ) { 55 | $meta = get_term_meta( $term, 'custom-key', true ); 56 | $this->assertEquals( 'my-value', $meta ); 57 | 58 | $status = get_term_meta( $term, $term_batch->slug . '_status', true ); 59 | $this->assertEquals( 'success', $status ); 60 | } 61 | 62 | // Run again so it skips some. 63 | $term_batch->run( 1 ); 64 | } 65 | 66 | /** 67 | * Test that you can clear individual result status. 68 | */ 69 | public function test_clear_term_result_status() { 70 | 71 | $term_batch = new Terms(); 72 | 73 | $term_batch->register( array( 74 | 'name' => 'Hey there', 75 | 'type' => 'term', 76 | 'callback' => __NAMESPACE__ . '\\my_term_callback_function_test', 77 | 'args' => array( 78 | 'number' => 5, 79 | 'taxonomy' => 'post_tag', 80 | 'hide_empty' => false, 81 | ), 82 | ) ); 83 | 84 | $term_batch->run( 1 ); 85 | 86 | $term_batch->clear_result_status(); 87 | 88 | // Loop through each post and make sure our value was set. 89 | foreach ( $this->tags as $term ) { 90 | $meta = get_term_meta( $term, 'custom-key', true ); 91 | $this->assertEquals( 'my-value', $meta ); 92 | 93 | $status = get_term_meta( $term, $term_batch->slug . '_status', true ); 94 | $this->assertEquals( '', $status ); 95 | } 96 | 97 | $batches = locomotive_get_all_batches(); 98 | $this->assertEquals( 'reset', $batches['hey-there']['status'] ); 99 | } 100 | 101 | /** 102 | * Test that terms offset batch gets run. 103 | */ 104 | public function test_terms_offset_run() { 105 | 106 | $term_batch = new Terms(); 107 | 108 | $term_batch->register( array( 109 | 'name' => 'Hey there', 110 | 'type' => 'term', 111 | 'callback' => __NAMESPACE__ . '\\my_term_callback_function_test', 112 | 'args' => array( 113 | 'number' => 3, 114 | 'offset' => 5, 115 | 'taxonomy' => 'category', 116 | 'hide_empty' => false, 117 | ), 118 | ) ); 119 | 120 | $batch_status = get_option( 'loco_batch_' . $term_batch->slug ); 121 | $this->assertFalse( $batch_status ); 122 | 123 | $term_batch->run( 2 ); 124 | 125 | $batch_status = get_option( 'loco_batch_' . $term_batch->slug ); 126 | $this->assertEquals( 'finished', $batch_status['status'] ); 127 | } 128 | 129 | /** 130 | * Test that batch gets run when data is destroyed during process. 131 | */ 132 | public function test_run_with_destructive_callback() { 133 | 134 | $term_batch = new Terms(); 135 | 136 | $term_batch->register( array( 137 | 'name' => 'Hey there', 138 | 'type' => 'term', 139 | 'callback' => __NAMESPACE__ . '\\my_callback_delete_term', 140 | 'args' => array( 141 | 'number' => 3, 142 | 'taxonomy' => 'post_tag', 143 | 'hide_empty' => false, 144 | ), 145 | ) ); 146 | 147 | // Simulate running twice with pass back of results number from client. 148 | $term_batch->run( 1 ); 149 | $_POST['total_num_results'] = 5; 150 | $term_batch->run( 2 ); 151 | 152 | // Check that all terms have been deleted. 153 | $all_terms = get_terms( array( 'taxonomy' => 'post_tag', 'hide_empty' => false, 'number' => 0 ) ); 154 | $this->assertCount( 0, $all_terms ); 155 | 156 | // Ensure that we are still getting a finished message. 157 | $batch_status = get_option( 'loco_batch_' . $term_batch->slug ); 158 | $this->assertEquals( 'finished', $batch_status['status'] ); 159 | } 160 | 161 | } 162 | 163 | /** 164 | * My callback function test for terms. 165 | * 166 | * @param WP_Term $result Result item. 167 | */ 168 | function my_term_callback_function_test( $result ) { 169 | update_term_meta( $result->term_id, 'custom-key', 'my-value' ); 170 | } 171 | 172 | /** 173 | * Callback function with destructive action (deletion). 174 | * 175 | * @param WP_Term $result Result item. 176 | */ 177 | function my_callback_delete_term( $result ) { 178 | wp_delete_term( $result->term_id, 'post_tag' ); 179 | } 180 | -------------------------------------------------------------------------------- /tests/test-functions.php: -------------------------------------------------------------------------------- 1 | register_successful_batch( '1' ); 26 | 27 | $all_batches = locomotive_get_all_batches(); 28 | $this->assertCount( 1, $all_batches ); 29 | 30 | register_batch_process( array( 31 | 'name' => 'My Test Batch process', 32 | 'type' => 'user', 33 | 'callback' => __NAMESPACE__ . '\\my_callback_function', 34 | 'args' => array( 35 | 'number' => 10, 36 | ), 37 | ) ); 38 | 39 | $all_batches = locomotive_get_all_batches(); 40 | $this->assertCount( 2, $all_batches ); 41 | } 42 | 43 | /** 44 | * Test that we can get all batches with proper data. 45 | */ 46 | public function test_all_batches() { 47 | $this->register_successful_batch( 'my-batch' ); 48 | $batches = locomotive_get_all_batches(); 49 | 50 | $this->assertNotNull( $batches['my-batch']['last_run'] ); 51 | $this->assertNotNull( $batches['my-batch']['status'] ); 52 | 53 | $post_batch = new Posts(); 54 | $post_batch->register( array( 55 | 'name' => 'Hey there', 56 | 'type' => 'post', 57 | 'callback' => __NAMESPACE__ . '\\my_callback_function', 58 | 'args' => array( 59 | 'posts_per_page' => 10, 60 | 'post_type' => 'post', 61 | ), 62 | ) ); 63 | 64 | $post_batch->run( 1 ); 65 | $batches = locomotive_get_all_batches(); 66 | 67 | $this->assertTrue( ( 'no results found' === $batches['hey-there']['status'] ) ); 68 | } 69 | 70 | /** 71 | * Test that we can clear existing batches. 72 | */ 73 | public function test_clear_batches() { 74 | $this->register_successful_batch( 'hello' ); 75 | $batches = locomotive_get_all_batches(); 76 | $this->assertCount( 1, $batches ); 77 | 78 | locomotive_clear_existing_batches(); 79 | $batches = locomotive_get_all_batches(); 80 | $this->assertCount( 0, $batches ); 81 | } 82 | 83 | /** 84 | * Test locomotive_time_ago() returns what we expect; 85 | */ 86 | public function test_time_ago() { 87 | $time = current_time( 'timestamp' ); 88 | $time_ago = locomotive_time_ago( $time ); 89 | 90 | $this->assertEquals( '1 min ago', $time_ago ); 91 | } 92 | 93 | /** 94 | * Confirm our CSS and JS assets are loading inside our settings page. 95 | */ 96 | public function test_asset_loading() { 97 | 98 | // Check that the items are not enquened before we start. 99 | $this->assertFalse( $this->are_batch_assets_enqueued() ); 100 | 101 | // Call our loader class on the locomotive settings page. 102 | $this->load_admin_enqueue_hook( 'tools_page_locomotive' ); 103 | 104 | // Check that the items are enquened. 105 | $this->assertTrue( $this->are_batch_assets_enqueued() ); 106 | } 107 | 108 | /** 109 | * Confirm our CSS and JS assets are not loading outside our settings page. 110 | */ 111 | public function test_asset_not_loading() { 112 | 113 | // Check that the items are not enquened before we start. 114 | $this->assertFalse( $this->are_batch_assets_enqueued() ); 115 | 116 | // Call our loader class on the general options page. 117 | $this->load_admin_enqueue_hook( 'options-general.php' ); 118 | 119 | // Check that our assets aren't enquened. 120 | $this->assertFalse( $this->are_batch_assets_enqueued() ); 121 | } 122 | 123 | /** 124 | * Test the time calculation by confirming a string is returned when passing a timestamp. 125 | */ 126 | public function test_elapsed_time() { 127 | $this->assertInternalType( 'string', locomotive_time_ago( 1472852621 ) ); 128 | } 129 | 130 | /** 131 | * Helper function to register a successful batch. 132 | * 133 | * @param string $slug Slug of test batch. 134 | */ 135 | private function register_successful_batch( $slug = 'test-batch' ) { 136 | register_batch_process( array( 137 | 'name' => 'My Test Batch process', 138 | 'slug' => $slug, 139 | 'type' => 'post', 140 | 'callback' => __NAMESPACE__ . '\\my_callback_function', 141 | 'args' => array( 142 | 'posts_per_page' => 10, 143 | 'post_type' => 'post', 144 | ), 145 | ) ); 146 | } 147 | 148 | /** 149 | * Helper function to call our script loader function. 150 | * 151 | * @param string $hook The hook that is passed to admin_enqueue_scripts. 152 | */ 153 | private function load_admin_enqueue_hook( $hook = '' ) { 154 | 155 | // Call our loader class. 156 | $this->admin_page = new Loader; 157 | 158 | // Set the admin hook to the requested page. 159 | $this->admin_page->scripts( $hook ); 160 | } 161 | 162 | /** 163 | * Helper function to check if the scripts and styles are loaded. 164 | * 165 | * @return bool Whether or not the style and script is enqueued. 166 | */ 167 | private function are_batch_assets_enqueued() { 168 | 169 | // Run the check on both the style and script enqueue. 170 | if ( false !== wp_style_is( 'batch-process-styles', 'enqueued' ) && false !== wp_script_is( 'batch-js', 'enqueued' ) ) { 171 | return true; 172 | } else { 173 | return false; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/types/test-batch-sites.php: -------------------------------------------------------------------------------- 1 | blogs = $this->factory->blog->create_many( 5 ); 23 | $this->user = $this->factory->user->create(); 24 | } 25 | 26 | protected function checkRequirements() { 27 | parent::checkRequirements(); 28 | 29 | $annotations = $this->getAnnotations(); 30 | 31 | foreach ( array( 'class', 'method' ) as $depth ) { 32 | if ( empty( $annotations[ $depth ]['requires'] ) ) { 33 | continue; 34 | } 35 | 36 | $requires = array_flip( $annotations[ $depth ]['requires'] ); 37 | if ( isset( $requires['Multisite'] ) && ! is_multisite() ) { 38 | $this->markTestSkipped( 'Multisite must be enabled' ); 39 | } 40 | } 41 | } 42 | 43 | 44 | /** 45 | * Test that site batch runs 46 | * 47 | * @requires Multisite 48 | */ 49 | public function test_site_finished_run() { 50 | $site_batch = new Sites(); 51 | 52 | $site_batch->register( array( 53 | 'name' => 'Hey there', 54 | 'type' => 'site', 55 | 'callback' => __NAMESPACE__ . '\\my_site_callback_function_test', 56 | 'args' => array( 57 | 'number' => 10, 58 | ), 59 | ) ); 60 | 61 | $batch_status = get_option( 'loco_batch_' . $site_batch->slug ); 62 | $this->assertFalse( $batch_status ); 63 | 64 | $site_batch->run( 1 ); 65 | 66 | $batch_status = get_option( 'loco_batch_' . $site_batch->slug ); 67 | $this->assertEquals( 'finished', $batch_status['status'] ); 68 | 69 | // Loop through each post and make sure our value was set. 70 | foreach ( $this->blogs as $site ) { 71 | $meta = get_metadata( 'site', $site, 'custom-key', true ); 72 | $this->assertEquals( 'my-value', $meta ); 73 | 74 | $status = get_metadata( 'site', $site, $site_batch->slug . '_status', true ); 75 | $this->assertEquals( 'success', $status ); 76 | } 77 | 78 | // Run again so it skips some. 79 | $site_batch->run( 1 ); 80 | } 81 | 82 | /** 83 | * Test that you can clear individual result status. 84 | * 85 | * @requires Multisite 86 | */ 87 | public function test_clear_site_result_status() { 88 | $site_batch = new Sites(); 89 | 90 | $site_batch->register( array( 91 | 'name' => 'Hey there', 92 | 'type' => 'site', 93 | 'callback' => __NAMESPACE__ . '\\my_site_callback_function_test', 94 | 'args' => array( 95 | 'number' => 7, 96 | ), 97 | ) ); 98 | 99 | $site_batch->run( 1 ); 100 | 101 | $site_batch->clear_result_status(); 102 | 103 | // Loop through each post and make sure our value was set. 104 | foreach ( $this->blogs as $site ) { 105 | $meta = get_metadata( 'site', $site, 'custom-key', true ); 106 | $this->assertEquals( 'my-value', $meta ); 107 | 108 | $status = get_metadata( 'site', $site, $site_batch->slug . '_status', true ); 109 | $this->assertEquals( '', $status ); 110 | } 111 | 112 | $batches = locomotive_get_all_batches(); 113 | $this->assertEquals( 'reset', $batches['hey-there']['status'] ); 114 | } 115 | 116 | /** 117 | * Test that sites offset batch gets run. 118 | * 119 | * @requires Multisite 120 | */ 121 | public function test_users_offset_run() { 122 | $site_batch = new Sites(); 123 | 124 | $site_batch->register( array( 125 | 'name' => 'Hey there', 126 | 'type' => 'site', 127 | 'callback' => __NAMESPACE__ . '\\my_site_callback_function_test', 128 | 'args' => array( 129 | 'number' => 3, 130 | 'offset' => 3, 131 | ), 132 | ) ); 133 | 134 | $batch_status = get_option( 'loco_batch_' . $site_batch->slug ); 135 | $this->assertFalse( $batch_status ); 136 | 137 | $site_batch->run( 2 ); 138 | 139 | $batch_status = get_option( 'loco_batch_' . $site_batch->slug ); 140 | $this->assertEquals( 'finished', $batch_status['status'] ); 141 | } 142 | 143 | /** 144 | * Test that batch gets run when data is destroyed during process. 145 | * 146 | * @requires Multisite 147 | */ 148 | public function test_run_with_destructive_callback() { 149 | 150 | $site_batch = new Sites(); 151 | 152 | // Offset to account for main site 153 | $site_batch->register( array( 154 | 'name' => 'Hey there', 155 | 'type' => 'site', 156 | 'callback' => __NAMESPACE__ . '\\my_callback_delete_site', 157 | 'args' => array( 158 | 'number' => 3, 159 | 'offset' => 1 160 | ), 161 | ) ); 162 | 163 | // Simulate running twice with pass back of results number from client. 164 | $site_batch->run( 1 ); 165 | $_POST['total_num_results'] = 5; 166 | $site_batch->run( 2 ); 167 | 168 | // Check that all sites have been deleted. 169 | // Ignore site ID 1 since that is the main site 170 | $all_sites = get_sites( array( 'site__not_in' => array( 1 ) ) ); 171 | $this->assertCount( 0, $all_sites ); 172 | 173 | // Ensure that we are still getting a finished message. 174 | $batch_status = get_option( 'loco_batch_' . $site_batch->slug ); 175 | $this->assertEquals( 'finished', $batch_status['status'] ); 176 | } 177 | 178 | } 179 | 180 | /** 181 | * My callback function test for sites. 182 | * 183 | * @param WP_Site $result Result item. 184 | */ 185 | function my_site_callback_function_test( $result ) { 186 | update_metadata( 'site', $result->blog_id, 'custom-key', 'my-value' ); 187 | } 188 | 189 | /** 190 | * Callback function with destructive action (deletion). 191 | * 192 | * @param WP_Site $result Result item. 193 | */ 194 | function my_callback_delete_site( $result ) { 195 | wpmu_delete_blog( $result->blog_id, true ); 196 | } 197 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Props to http://mikevalstar.com/post/fast-gulp-browserify-babelify-watchify-react-build/ for most of this file. 2 | 'use strict'; 3 | 4 | var gulp = require( 'gulp' ); // Base gulp package. 5 | var babelify = require( 'babelify' ); // Used to convert ES6 & JSX to ES5. 6 | var browserify = require( 'browserify' ); // Providers "require" support, CommonJS. 7 | var notify = require( 'gulp-notify' ); // Provides notification to both the console and Growel. 8 | var rename = require( 'gulp-rename' ); // Rename sources. 9 | var sourcemaps = require( 'gulp-sourcemaps' ); // Provide external sourcemap files. 10 | var livereload = require( 'gulp-livereload' ); // Livereload support for the browser. 11 | var gutil = require( 'gulp-util' ); // Provides gulp utilities, including logging and beep. 12 | var chalk = require( 'chalk' ); // Allows for coloring for logging. 13 | var source = require( 'vinyl-source-stream' ); // Vinyl stream support. 14 | var buffer = require( 'vinyl-buffer' ); // Vinyl stream support. 15 | var watchify = require( 'watchify' ); // Watchify for source changes. 16 | var merge = require( 'utils-merge' ); // Object merge tool. 17 | var duration = require( 'gulp-duration' ); // Time aspects of your gulp process. 18 | var uglify = require( 'gulp-uglify' ); // Minify the JS. 19 | var phpcs = require( 'gulp-phpcs' ); // Verify the PHP Coding Standards 20 | var phplint = require( 'phplint' ).lint; // Lint PHP. 21 | var shell = require( 'gulp-shell' ); // Run shell commands. 22 | var runSequence = require( 'run-sequence' ); // Run tasks in series. 23 | var wpPot = require( 'gulp-wp-pot' ); // Run our localization setup. 24 | var sort = require( 'gulp-sort' ); // Run the sorting function used in the localization. 25 | var eslint = require( 'gulp-eslint' ); 26 | 27 | // Configuration for Gulp. 28 | var config = { 29 | js: { 30 | src: 'assets/src/js/batch.jsx', 31 | watch: 'assets/src/js/**/*', 32 | outputDir: 'assets/dist/', 33 | outputFile: 'batch.min.js' 34 | } 35 | }; 36 | 37 | /** 38 | * Task to handle the JavaScript building and sourcemap generating. 39 | */ 40 | gulp.task( 'build', function() { 41 | var args = { debug: true }; 42 | var bundler = browserify( config.js.src, args ) 43 | .transform( babelify, { 44 | presets: ['es2015', 'react'] 45 | } ); 46 | 47 | bundle( bundler ); 48 | } ); 49 | 50 | /** 51 | * Task to handle linting Javascript 52 | */ 53 | gulp.task( 'eslint', function() { 54 | return gulp.src([ 'assets/src/**/*.js', 'assets/src/**/*.jsx' ]) 55 | .pipe( eslint() ) 56 | .pipe( eslint.format() ) 57 | .pipe( eslint.failAfterError() ) 58 | }); 59 | 60 | /** 61 | * Task to handle the file creation for localization. 62 | */ 63 | gulp.task( 'localize', function() { 64 | return gulp.src([ '*.php', 'includes/**/*.php', 'templates/**/*.php' ]) 65 | .pipe( sort() ) 66 | .pipe( wpPot( { 67 | domain: 'locomotive', 68 | destFile:'locomotive.pot', 69 | package: 'locomotive', 70 | bugReport: 'https://github.com/reaktivstudios/locomotive/issues', 71 | lastTranslator: 'Andrew Norcross ', 72 | team: 'Reaktiv Studios ' 73 | } )) 74 | .pipe( gulp.dest( 'languages' ) ); 75 | }); 76 | 77 | /** 78 | * Task to handle the JavaScript building and sourcemap generating. 79 | */ 80 | gulp.task( 'watch', function() { 81 | var args = merge( watchify.args, { debug: true } ); // Merge in default watchify args with browserify arguments. 82 | var bundler = browserify( config.js.src, args ) // Browserify. 83 | .plugin( watchify, { 84 | ignoreWatch: ['**/node_modules/**', '**/bower_components/**'] 85 | } ) // Watchify to watch source file changes. 86 | .transform( babelify, { 87 | presets: ['es2015', 'react'] 88 | } ); 89 | 90 | livereload.listen(); // Start livereload server. 91 | 92 | bundle( bundler ); // Run the bundle the first time (required for Watchify to kick in). 93 | 94 | bundler.on( 'update', function() { 95 | bundle( bundler ); // Re-run bundle on source updates. 96 | }); 97 | } ); 98 | 99 | // Completes the final file outputs. 100 | function bundle( bundler ) { 101 | var bundleTimer = duration( 'Javascript bundle time' ); 102 | 103 | return bundler 104 | .bundle() 105 | .on( 'error', function( err ) { 106 | console.error( err ); this.emit( 'end' ); 107 | }) 108 | .pipe( source( 'batch.jsx' ) ) // Set source name. 109 | .pipe( buffer() ) // Convert to gulp pipeline. 110 | .pipe( rename( config.js.outputFile ) ) // Rename the output file. 111 | .pipe( sourcemaps.init( { loadMaps: true } ) ) // Extract the inline sourcemaps. 112 | .pipe( uglify() ) // Minify the JS. 113 | .pipe( sourcemaps.write( './map' ) ) // Set folder for sourcemaps to output to. 114 | .pipe( gulp.dest( config.js.outputDir ) ) // Set the output folder. 115 | .pipe( notify( { 116 | message: 'Generated file: <%= file.relative %>' 117 | } ) ) // Output the file being created. 118 | .pipe( bundleTimer ) // Output time timing of the file creation. 119 | .pipe( livereload() ); // Reload the view in the browser. 120 | } 121 | 122 | // Verify PHP Coding Standards. 123 | gulp.task( 'phpcs', function() { 124 | return gulp.src( [ 125 | '**/*.php', 126 | '!node_modules/**/*.*', 127 | '!tests/**/*.*', 128 | '!vendor/**/*.*' 129 | ] ) 130 | .pipe( phpcs( { 131 | bin: 'vendor/bin/phpcs', 132 | standard: 'ruleset.xml' 133 | } ) ) 134 | .pipe( phpcs.reporter( 'log' ) ) 135 | .pipe( phpcs.reporter( 'fail' ) ); 136 | } ); 137 | 138 | // Lint PHP. 139 | gulp.task( 'phplint', function( cb ) { 140 | phplint( [ 141 | '**/*.php', 142 | '!node_modules/**/*.*', 143 | '!tests/**/*.*', 144 | '!vendor/**/*.*' 145 | ], { limit: 10 }, function( err, stdout, stderr ) { 146 | if ( err ) { 147 | cb( err ); 148 | process.exit( 1 ); 149 | } 150 | cb(); 151 | } ); 152 | } ); 153 | 154 | // Run single site PHPUnit tests. 155 | gulp.task( 'phpunit-single', shell.task( [ 'vendor/bin/phpunit -c phpunit.xml.dist' ] ) ); 156 | 157 | // Run multisite PHPUnit tests. 158 | gulp.task( 'phpunit-multisite', shell.task( [ 'vendor/bin/phpunit -c multisite.xml.dist' ] ) ); 159 | 160 | // Run single site PHPUnit tests with code coverage. 161 | gulp.task( 'phpunit-codecoverage', shell.task( [ 'vendor/bin/phpunit -c codecoverage.xml.dist' ] ) ); 162 | 163 | // Gulp task for build 164 | gulp.task( 'default', [ 'eslint', 'build' ] ); 165 | 166 | // Lint files. 167 | gulp.task( 'lint', [ 'phplint', 'phpcs', 'eslint' ] ); 168 | 169 | // Run PHP tests. 170 | gulp.task( 'phpunit', function() { 171 | runSequence( 'phpunit-single', 'phpunit-multisite' ); 172 | } ); 173 | 174 | // Run full build and test. 175 | gulp.task( 'test', function() { 176 | runSequence( 'build', 'lint', 'phpunit' ); 177 | } ); 178 | -------------------------------------------------------------------------------- /locomotive.php: -------------------------------------------------------------------------------- 1 | id ) || 'tools_page_locomotive' !== $screen->id ) { 75 | return; 76 | } 77 | 78 | // Load our actual help tab. 79 | $screen->add_help_tab( array( 80 | 'id' => 'help-overview', 81 | 'title' => esc_html__( 'Overview', 'locomotive' ), 82 | 'content' => '

    ' . esc_html__( 'Locomotive is a batch processing library that can be used to write a single or set of functions to be processed across a large data set, right from the WordPress admin. You can use it to add meta values to posts based on arbitrary data, process and delete spam comments and revisions, submit posts through external API\'s, or simply change data on a large amount of posts at the same time.', 'locomotive' ) . '

    ', 83 | ) ); 84 | 85 | // Set up the text for the sidebar. 86 | $side = '

    ' . esc_html__( 'More on GitHub:', 'locomotive' ) . '

    '; 87 | $side .= ''; 93 | 94 | // Send through our filter. 95 | $side = apply_filters( 'loco_help_tab_sidebar', $side ); 96 | 97 | // Load the sidebar portion of the help tab. 98 | $screen->set_help_sidebar( $side ); 99 | } 100 | 101 | /** 102 | * Load in all the files we need. 103 | */ 104 | public function load_includes() { 105 | require_once( LOCO_PLUGIN_DIR . 'includes/abstracts/abstract-batch.php' ); 106 | require_once( LOCO_PLUGIN_DIR . 'includes/batches/class-batch-posts.php' ); 107 | require_once( LOCO_PLUGIN_DIR . 'includes/batches/class-batch-users.php' ); 108 | require_once( LOCO_PLUGIN_DIR . 'includes/batches/class-batch-sites.php' ); 109 | require_once( LOCO_PLUGIN_DIR . 'includes/batches/class-batch-terms.php' ); 110 | require_once( LOCO_PLUGIN_DIR . 'includes/batches/class-batch-comments.php' ); 111 | require_once( LOCO_PLUGIN_DIR . 'includes/functions.php' ); 112 | } 113 | 114 | /** 115 | * Handle hooks. 116 | */ 117 | public function attach_hooks() { 118 | add_action( 'admin_menu', array( $this, 'add_dashboard' ) ); 119 | add_action( 'after_setup_theme', array( $this, 'loaded' ) ); 120 | add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) ); 121 | 122 | add_action( 'wp_ajax_run_batch', array( $this, 'run' ) ); 123 | add_action( 'wp_ajax_reset_batch', array( $this, 'reset' ) ); 124 | } 125 | 126 | /** 127 | * Plugin stylesheet and JavaScript. 128 | * 129 | * @param string $hook The current page loaded in the WP admin. 130 | */ 131 | public function scripts( $hook ) { 132 | 133 | // Exclude our scripts and CSS files from loading globally. 134 | if ( 'tools_page_locomotive' !== $hook ) { 135 | return; 136 | } 137 | 138 | wp_enqueue_style( 'batch-process-styles', LOCO_PLUGIN_URL . 'assets/main.css' ); 139 | wp_enqueue_script( 'batch-js', LOCO_PLUGIN_URL . 'assets/dist/batch.min.js', array( 'jquery' ), '0.1.0', true ); 140 | 141 | wp_localize_script( 'batch-js', 'batch', array( 142 | 'nonce' => wp_create_nonce( 'run-batch-process' ), 143 | 'ajaxurl' => admin_url( 'admin-ajax.php' ), 144 | 'batches' => locomotive_get_all_batches(), 145 | 'page_title' => esc_html( get_admin_page_title() ), 146 | ) ); 147 | } 148 | 149 | /** 150 | * Let everyone know we are loaded and ready to go. 151 | */ 152 | public function loaded() { 153 | if ( is_admin() ) { 154 | locomotive_clear_existing_batches(); 155 | do_action( 'locomotive_init' ); 156 | } 157 | } 158 | 159 | /** 160 | * AJAX handler for running a batch. 161 | * 162 | * @todo Move this to it's own AJAX class. 163 | */ 164 | public function run() { 165 | $batch_process = ''; 166 | $step = 0; 167 | $errors = array(); 168 | 169 | check_ajax_referer( 'run-batch-process', 'nonce' ); 170 | 171 | if ( empty( $_POST['batch_process'] ) ) { 172 | $errors[] = __( 'Batch process not specified.', 'locomotive' ); 173 | } else { 174 | $batch_process = sanitize_text_field( wp_unslash( $_POST['batch_process'] ) ); 175 | } 176 | 177 | if ( empty( $_POST['step'] ) ) { 178 | $errors[] = __( 'Step must be defined.', 'locomotive' ); 179 | } else { 180 | $step = absint( $_POST['step'] ); 181 | } 182 | 183 | if ( ! empty( $errors ) ) { 184 | wp_send_json( array( 185 | 'success' => false, 186 | 'errors' => $errors, 187 | ) ); 188 | } 189 | 190 | do_action( 'loco_batch_' . $batch_process, $step ); 191 | } 192 | 193 | /** 194 | * AJAX handler for running a batch. 195 | * 196 | * @todo Move this to it's own AJAX class. 197 | */ 198 | public function reset() { 199 | $batch_process = ''; 200 | $errors = array(); 201 | 202 | check_ajax_referer( 'run-batch-process', 'nonce' ); 203 | 204 | if ( empty( $_POST['batch_process'] ) ) { 205 | $errors[] = __( 'Batch process not specified.', 'locomotive' ); 206 | } else { 207 | $batch_process = sanitize_text_field( wp_unslash( $_POST['batch_process'] ) ); 208 | } 209 | 210 | if ( ! empty( $errors ) ) { 211 | wp_send_json( array( 212 | 'success' => false, 213 | 'errors' => $errors, 214 | ) ); 215 | } 216 | 217 | do_action( 'loco_batch_' . $batch_process . '_reset' ); 218 | 219 | wp_send_json( array( 'success' => true ) ); 220 | } 221 | 222 | /** 223 | * Init. 224 | */ 225 | public function init() { 226 | $this->define_constants(); 227 | $this->load_includes(); 228 | $this->attach_hooks(); 229 | } 230 | } 231 | 232 | $batch_processing = new Loader(); 233 | $batch_processing->init(); 234 | -------------------------------------------------------------------------------- /assets/src/js/batch.jsx: -------------------------------------------------------------------------------- 1 | var React = require( 'react' ); 2 | var ReactDOM = require( 'react-dom' ); 3 | var $ = jQuery; // We are loading this file after jQuery through `wp_enqueue_script`. 4 | 5 | /** 6 | * Application components. 7 | */ 8 | import BatchPicker from './components/BatchPicker'; 9 | import Modal from './components/Modal'; 10 | import ModalReset from './components/ModalReset'; 11 | 12 | /** 13 | * Our Locomotive App. 14 | */ 15 | var App = React.createClass( { 16 | getInitialState : function () { 17 | return { 18 | // Batches that can be run. Loaded through `wp_localize_script()`. 19 | batches: batch.batches, 20 | page_title: batch.page_title, 21 | 22 | // Object to hold data relating to running a migration (in the modal). 23 | processing: { 24 | active: false, 25 | batch: false, 26 | 27 | // Remote data is data that is retrieved via Ajax calls during a 28 | // batch process. 29 | remote_data: { 30 | batch_title: '', 31 | status: '', 32 | error: false, 33 | progress: 0, 34 | current_step: 0, 35 | total_steps: 0, 36 | total_num_results: 0 37 | } 38 | }, 39 | errors: [], 40 | resetActive: false 41 | }; 42 | }, 43 | 44 | /** 45 | * Update which batch is currently selected. 46 | * 47 | * @param key Currently selected batch key. 48 | */ 49 | updateSelectedBatch : function ( key ) { 50 | if ( this.state.processing.active ) { 51 | return; 52 | } 53 | 54 | this.state.processing.batch = key; 55 | this.setState( { processing: this.state.processing } ); 56 | }, 57 | 58 | /** 59 | * Function to handle flipping the switches for the modal. 60 | * 61 | * @param bool active Whether or not we are processing a batch. 62 | */ 63 | toggleProcessing : function ( active ) { 64 | if ( false === active || true === active ) { 65 | this.state.processing.active = active; 66 | this.setState( { processing: this.state.processing } ); 67 | } 68 | }, 69 | 70 | /** 71 | * Run the currently selected batch process. 72 | */ 73 | runBatch : function ( currentStep ) { 74 | if ( '' === this.state.processing.batch ) { 75 | return; 76 | } 77 | 78 | var self = this, 79 | batchSlug = self.state.processing.batch, 80 | totalResults = self.state.processing.remote_data.total_num_results; 81 | 82 | // If we open the modal and it was previously complete, clear it. 83 | if ( 100 === this.state.processing.remote_data.progress ) { 84 | this.state.processing.batch = ''; 85 | this.state.processing.remote_data = { 86 | progress: 0 87 | }; 88 | 89 | this.setState( { processing: this.state.processing, errors: [] } ); 90 | } 91 | 92 | 93 | 94 | $.ajax( { 95 | type: 'POST', 96 | url: batch.ajaxurl, 97 | data: { 98 | batch_process: batchSlug, 99 | nonce: batch.nonce, 100 | step: currentStep, 101 | total_num_results: totalResults, 102 | action: 'run_batch', 103 | }, 104 | dataType: 'json', 105 | success: function ( response ) { 106 | // Update our state with the processing status and progress, which will update the modal. 107 | self.state.processing.batch = batchSlug; 108 | self.state.processing.remote_data = { 109 | batch_title: response.batch, 110 | status: response.status, 111 | progress: response.progress, 112 | current_step: response.current_step, 113 | total_steps: response.total_steps, 114 | total_num_results: response.total_num_results, 115 | error: response.error 116 | }; 117 | 118 | // Update our batches, which will update the batch listing. 119 | self.state.batches[ batchSlug ].last_run = 'just ran'; 120 | self.state.batches[ batchSlug ].status = self.state.processing.remote_data.status; 121 | 122 | // Check for errors. 123 | if ( response.error ) { 124 | self.setState( { errors: self.state.errors.concat( response.errors ) } ); 125 | self.setState( { processing: self.state.processing } ); 126 | } 127 | 128 | self.setState( { 129 | processing: self.state.processing, 130 | batches: self.state.batches 131 | } ); 132 | // Determine if we have to run another step in the batch. Checks if there are more steps 133 | // that need to run and makes sure the 'status' from the server is still 'running'. 134 | if ( response.currentStep !== response.total_steps && 'running' === response.status.toLowerCase() ) { 135 | self.runBatch( response.current_step + 1 ); 136 | } 137 | } 138 | } ).fail( function () { 139 | alert( 'Something went wrong.' ); 140 | } ); 141 | 142 | this.toggleProcessing( true ); 143 | }, 144 | 145 | toggleResetModal : function ( active ) { 146 | if ( false === active || true === active ) { 147 | this.setState( { resetActive: active } ); 148 | } 149 | }, 150 | /** 151 | * Reset the selected batch process. 152 | */ 153 | resetBatch : function () { 154 | if ( '' === this.state.processing.batch ) { 155 | return; 156 | } 157 | 158 | this.setState( { resetActive: false } ); 159 | 160 | var self = this, 161 | batchSlug = self.state.processing.batch.toString(); 162 | 163 | $.ajax( { 164 | type: 'POST', 165 | url: batch.ajaxurl, 166 | data: { 167 | batch_process: batchSlug, 168 | nonce: batch.nonce, 169 | action: 'reset_batch', 170 | }, 171 | dataType: 'json', 172 | success: function ( response ) { 173 | if ( response.success ) { 174 | // Update our batches, which will update the batch listing. 175 | self.state.batches[ self.state.processing.batch ].last_run = 'never'; 176 | self.state.batches[ self.state.processing.batch ].status = 'new'; 177 | 178 | self.setState( { 179 | processing: self.state.processing, 180 | batches: self.state.batches 181 | } ); 182 | } else { 183 | alert( 'Reset batch failed.' ); 184 | } 185 | } 186 | } ).fail( function () { 187 | alert( 'Something went wrong.' ); 188 | } ); 189 | }, 190 | 191 | /** 192 | * Decides if `Run Batch` button is enabled or disabled. 193 | * 194 | * @returns {boolean} Can we run a batch? 195 | */ 196 | canInteractWithBatch : function () { 197 | // Default to being able to run a batch. 198 | var canRun = true; 199 | var processingState = this.state.processing; 200 | 201 | // If we don't have a batch selected. 202 | if ( false === processingState.batch ) { 203 | canRun = false; 204 | } 205 | 206 | // If we are have the modal open. 207 | if ( processingState.active ) { 208 | canRun = false; 209 | } 210 | 211 | // If we are currently processing a batch and there are results. 212 | if ( processingState.remote_data.current_step !== processingState.remote_data.total_steps && 0 !== processingState.remote_data.total_num_results ) { 213 | canRun = false; 214 | } 215 | 216 | return canRun; 217 | }, 218 | 219 | /** 220 | * Render our batch application. 221 | * 222 | * @returns {JSX} 223 | */ 224 | render : function () { 225 | var selectedBatch = ''; 226 | if ( this.state.processing.batch ) { 227 | selectedBatch = this.state.batches[ this.state.processing.batch ].name; 228 | } 229 | 230 | return ( 231 |
    232 |

    { this.state.page_title }

    233 | 240 | 241 | 248 | 249 | 254 |
    255 | ); 256 | } 257 | } ); 258 | 259 | ReactDOM.render( , document.getElementById( 'batch-main' ) ); 260 | -------------------------------------------------------------------------------- /tests/test-batch.php: -------------------------------------------------------------------------------- 1 | register( array( 28 | 'slug' => 'test-anot22h12er-batch', 29 | 'type' => 'post', 30 | 'callback' => 'my_callback_function', 31 | 'args' => array( 32 | 'posts_per_page' => 10, 33 | 'post_type' => 'post', 34 | ), 35 | ) ); 36 | } catch ( Exception $e ) { 37 | $this->assertEquals( 'Batch name must be defined.', $e->getMessage() ); 38 | } 39 | 40 | } 41 | 42 | /** 43 | * Test type is included. 44 | */ 45 | public function test_register_batch_includes_type() { 46 | try { 47 | $batch_process = new Posts(); 48 | $batch_process->register( array( 49 | 'name' => 'Hey', 50 | 'slug' => 'test-anot22h12er-batch', 51 | 'callback' => 'my_callback_function', 52 | 'args' => array( 53 | 'posts_per_page' => 10, 54 | 'post_type' => 'post', 55 | ), 56 | ) ); 57 | } catch ( Exception $e ) { 58 | $this->assertEquals( 'Batch type must be defined.', $e->getMessage() ); 59 | } 60 | } 61 | 62 | /** 63 | * Test callback defined 64 | */ 65 | public function test_register_batch_includes_callback() { 66 | try { 67 | $batch_process = new Posts(); 68 | $batch_process->register( array( 69 | 'name' => 'Hey', 70 | 'type' => 'post', 71 | 'slug' => 'test-anot22h12er-batch', 72 | 'args' => 0, 73 | ) ); 74 | } catch ( Exception $e ) { 75 | $this->assertEquals( 'A callback must be defined.', $e->getMessage() ); 76 | } 77 | } 78 | 79 | /** 80 | * Test without arguments 81 | */ 82 | public function test_register_batch_doesnt_require_args() { 83 | $batch_process = new Posts(); 84 | $batch_process->register( array( 85 | 'name' => 'Hey there', 86 | 'type' => 'post', 87 | 'callback' => 'my_callback_function', 88 | ) ); 89 | 90 | $this->assertNotNull( $batch_process->currently_registered['hey-there'] ); 91 | } 92 | 93 | /** 94 | * Make sure slugs get slashes. 95 | */ 96 | public function test_register_batch_no_slug_gets_name() { 97 | $batch_process = new Posts(); 98 | $batch_process->register( array( 99 | 'name' => 'Hey there', 100 | 'type' => 'post', 101 | 'callback' => 'my_callback_function', 102 | 'args' => array( 103 | 'posts_per_page' => 10, 104 | 'post_type' => 'post', 105 | ), 106 | ) ); 107 | 108 | $this->assertNotNull( $batch_process->currently_registered['hey-there'] ); 109 | } 110 | 111 | /** 112 | * Make sure adding batch adds to currently registered. 113 | */ 114 | public function test_register_overwrites_currently_registered_if_same_slug() { 115 | $this->register_successful_batch( 'hey' ); 116 | 117 | $batch = new Posts(); 118 | $batch->register( array( 119 | 'name' => 'My Test Batch process OVERWRITE', 120 | 'slug' => 'hey', 121 | 'type' => 'post', 122 | 'callback' => 'my_callback_function', 123 | 'args' => array( 124 | 'posts_per_page' => 10, 125 | 'post_type' => 'post', 126 | ), 127 | ) ); 128 | 129 | $this->assertEquals( 'My Test Batch process OVERWRITE', $batch->currently_registered['hey']['name'] ); 130 | } 131 | 132 | /** 133 | * Make sure that when a batch process is registered that `currently_registered` 134 | * is an array. 135 | */ 136 | public function test_empty_currently_registered_is_array_when_new_batch_added() { 137 | locomotive_clear_existing_batches(); 138 | 139 | $batch_process = new Posts(); 140 | $this->assertTrue( is_array( $batch_process->currently_registered ) ); 141 | } 142 | 143 | /** 144 | * Test that status gets updated on a batch to no results found. 145 | */ 146 | public function test_no_results_found() { 147 | $post_batch = new Posts(); 148 | 149 | $post_batch->register( array( 150 | 'name' => 'Hey there', 151 | 'type' => 'post', 152 | 'callback' => 'my_callback_function_test', 153 | 'args' => array( 154 | 'posts_per_page' => 10, 155 | 'post_type' => 'post', 156 | ), 157 | ) ); 158 | 159 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 160 | $this->assertFalse( $batch_status ); 161 | 162 | $post_batch->run( 1 ); 163 | 164 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 165 | $this->assertEquals( 'no results found', $batch_status['status'] ); 166 | } 167 | 168 | /** 169 | * Test that batch gets run. 170 | */ 171 | public function test_running_run() { 172 | // Create 5 posts. 173 | for ( $x = 0; $x < 15; $x++ ) { 174 | $this->factory->post->create(); 175 | } 176 | 177 | $post_batch = new Posts(); 178 | 179 | $post_batch->register( array( 180 | 'name' => 'Hey there', 181 | 'type' => 'post', 182 | 'callback' => 'my_callback_function_test', 183 | 'args' => array( 184 | 'posts_per_page' => 10, 185 | 'post_type' => 'post', 186 | ), 187 | ) ); 188 | 189 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 190 | $this->assertFalse( $batch_status ); 191 | 192 | $post_batch->run( 1 ); 193 | 194 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 195 | $this->assertEquals( 'running', $batch_status['status'] ); 196 | } 197 | 198 | /** 199 | * Test that offset is applied properly to each step 200 | */ 201 | public function test_for_proper_offset() { 202 | $this->factory->post->create_many( 10 ); 203 | 204 | $post_batch = new Posts(); 205 | 206 | $post_batch->register( array( 207 | 'name' => 'Hey there', 208 | 'type' => 'post', 209 | 'callback' => __NAMESPACE__ . '\\my_post_callback_function_test', 210 | 'args' => array( 211 | 'posts_per_page' => 5, 212 | 'post_type' => 'post', 213 | ), 214 | ) ); 215 | 216 | $first_run = $post_batch->run( 1 ); 217 | $second_run = $post_batch->run( 2 ); 218 | 219 | // Results from the first and second run should NOT match. 220 | $this->assertNotEquals( $second_run['query_results'][0]->ID, $first_run['query_results'][0]->ID ); 221 | 222 | } 223 | 224 | /** 225 | * Test that batch gets run. 226 | */ 227 | public function test_run_with_error() { 228 | $posts = array(); 229 | 230 | // Create 5 posts. 231 | for ( $x = 0; $x < 5; $x++ ) { 232 | $posts[] = $this->factory->post->create(); 233 | } 234 | 235 | $post_batch = new Posts(); 236 | 237 | $post_batch->register( array( 238 | 'name' => 'Hey there', 239 | 'type' => 'post', 240 | 'callback' => __NAMESPACE__ . '\\my_callback_function_test_false', 241 | 'args' => array( 242 | 'posts_per_page' => 10, 243 | 'post_type' => 'post', 244 | ), 245 | ) ); 246 | 247 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 248 | $this->assertFalse( $batch_status ); 249 | 250 | $run = $post_batch->run( 1 ); 251 | 252 | // Error is returned. 253 | $this->assertArrayHasKey( 'errors', $run ); 254 | 255 | // Errors return matches class errors object. 256 | $this->assertCount( 5, $run['errors'] ); 257 | $this->assertCount( 5, $post_batch->result_errors ); 258 | 259 | // Sample message is returned properly. 260 | $this->assertEquals( 'Not working', $run['errors'][0]['message'] ); 261 | 262 | $batch_status = get_option( 'loco_batch_' . $post_batch->slug ); 263 | $this->assertEquals( 'finished', $batch_status['status'] ); 264 | } 265 | 266 | 267 | /** 268 | * Helper function to register a successful batch. 269 | * 270 | * @param string $slug Slug of test batch. 271 | */ 272 | private function register_successful_batch( $slug = 'test-batch' ) { 273 | $batch = new Posts(); 274 | $batch->register( array( 275 | 'name' => 'My Test Batch process', 276 | 'slug' => $slug, 277 | 'type' => 'post', 278 | 'callback' => 'my_callback_function', 279 | 'args' => array( 280 | 'posts_per_page' => 10, 281 | 'post_type' => 'post', 282 | ), 283 | ) ); 284 | 285 | return $batch; 286 | } 287 | 288 | } 289 | 290 | /** 291 | * My callback function test. 292 | * 293 | * @param WP_Post $result Result item. 294 | */ 295 | function my_callback_function_test( $result ) { 296 | update_post_meta( $result->ID, 'custom-key', 'my-value' ); 297 | } 298 | 299 | /** 300 | * My callback function test. 301 | * 302 | * @param object $result Result item. 303 | * @throws \Exception Not working. 304 | */ 305 | function my_callback_function_test_false( $result ) { 306 | throw new \Exception( 'Not working' ); 307 | } 308 | -------------------------------------------------------------------------------- /includes/abstracts/abstract-batch.php: -------------------------------------------------------------------------------- 1 | args = wp_parse_args( $this->args, $this->default_args ); 158 | $this->batch_get_results(); 159 | $this->calculate_offset(); 160 | 161 | // Run query again, but this time with the new offset calculated. 162 | $results = $this->batch_get_results(); 163 | return $results; 164 | } 165 | 166 | /** 167 | * Set the total number of results 168 | * 169 | * Uses a number passed from the client to the server and compares it to the total objects 170 | * pulled by the latest query. If the dataset is larger, we increase the total_num_results number. 171 | * Otherwise, keep it at the original (to account for deletion / changes). 172 | * 173 | * @param int $total_from_query Total number of results from latest query. 174 | */ 175 | public function set_total_num_results( $total_from_query ) { 176 | // If this is past step 1, the client is passing back the total number of results. 177 | // This accounts for deletion / destructive actions to the data. 178 | $total_from_request = isset( $_POST['total_num_results'] ) ? absint( $_POST['total_num_results'] ) : 0; // Input var okay. 179 | 180 | // In all cases we want to ensure that we use the higher of the two results total (from client or query). 181 | // We go with the higher number because we want to lock the total number of steps calculated at it's highest total. 182 | // With a destructive action, that would be total from request. If addivitve action, it would be total from query. 183 | // In all other cases, these two numbers are equal, so either would work. 184 | if ( $total_from_query > $total_from_request ) { 185 | $this->total_num_results = (int) $total_from_query; 186 | } else { 187 | $this->total_num_results = (int) $total_from_request; 188 | } 189 | 190 | $this->record_change_if_totals_differ( $total_from_request, $total_from_query ); 191 | } 192 | 193 | /** 194 | * If the amount of total records has changed, the amount is recorded so that it can 195 | * be applied to the offeset when it is calculated. This ensures that the offset takes into 196 | * account if new objects have been added or removed from the query. 197 | * 198 | * @param int $total_from_request Total number of results passed up from client. 199 | * @param int $total_from_query Total number of results retreived from query. 200 | */ 201 | public function record_change_if_totals_differ( $total_from_request, $total_from_query ) { 202 | if ( $total_from_query !== $total_from_request && $total_from_request > 0 ) { 203 | $this->difference_in_result_totals = $total_from_request - $total_from_query; 204 | } 205 | } 206 | 207 | /** 208 | * Calculate the offset for the current query. 209 | */ 210 | public function calculate_offset() { 211 | if ( 1 !== $this->current_step ) { 212 | // Example: step 2: 1 * 10 = offset of 10, step 3: 2 * 10 = offset of 20. 213 | // The difference in result totals is used in case of additive or destructive actions. 214 | // if 5 posts were deleted in step 1 (20 - 15 = 5) then the offset should remain at 0 ( offset of 5 - 5) in step 2. 215 | $this->args['offset'] = ( ( $this->current_step - 1 ) * $this->args[ $this->per_batch_param ] ) - $this->difference_in_result_totals; 216 | } 217 | } 218 | 219 | /** 220 | * Register the batch process so we can run it. 221 | * 222 | * @param array $args Details about the batch you are registering. 223 | */ 224 | public function register( $args ) { 225 | if ( $this->setup( $args ) ) { 226 | if ( ! defined( 'DOING_AJAX' ) ) { 227 | $this->add(); 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * Add a batch process to our system. 234 | */ 235 | private function add() { 236 | if ( ! isset( $this->currently_registered[ $this->slug ] ) ) { 237 | $this->currently_registered[ $this->slug ] = array( 238 | 'name' => $this->name, 239 | ); 240 | } else { 241 | $this->currently_registered[ $this->slug ]['name'] = $this->name; 242 | } 243 | 244 | return locomotive_update_registered_batches( $this->currently_registered ); 245 | } 246 | 247 | /** 248 | * Setup our Batch object to have everything it needs (callback, name, slug, 249 | * etc). 250 | * 251 | * @todo Research the best way to handle exceptions. 252 | * 253 | * @param array $args Array of args for register. 254 | * @throws Exception Type must be provided. 255 | * @return true|exception 256 | */ 257 | private function setup( $args ) { 258 | if ( empty( $args['name'] ) ) { 259 | throw new Exception( __( 'Batch name must be defined.', 'locomotive' ) ); 260 | } else { 261 | $this->name = $args['name']; 262 | } 263 | 264 | if ( empty( $args['slug'] ) ) { 265 | $this->slug = sanitize_title_with_dashes( $args['name'] ); 266 | } else { 267 | $this->slug = $args['slug']; 268 | } 269 | 270 | if ( empty( $args['type'] ) ) { 271 | throw new Exception( __( 'Batch type must be defined.', 'locomotive' ) ); 272 | } else { 273 | $this->type = $args['type']; 274 | } 275 | 276 | if ( empty( $args['args'] ) || ! is_array( $args['args'] ) ) { 277 | $this->args = array(); 278 | } else { 279 | $this->args = $args['args']; 280 | } 281 | 282 | if ( empty( $args['callback'] ) ) { 283 | throw new Exception( __( 'A callback must be defined.', 'locomotive' ) ); 284 | } else { 285 | $this->callback = $args['callback']; 286 | } 287 | 288 | $this->currently_registered = locomotive_get_all_batches(); 289 | 290 | add_action( 'loco_batch_' . $this->slug, array( $this, 'run_ajax' ) ); 291 | add_action( 'loco_batch_' . $this->slug . '_reset', array( $this, 'clear_result_status' ) ); 292 | 293 | return true; 294 | } 295 | 296 | /** 297 | * Return JSON for AJAX requests to run. 298 | * 299 | * @param int $current_step Current step. 300 | */ 301 | public function run_ajax( $current_step ) { 302 | wp_send_json( $this->run( $current_step ) ); 303 | } 304 | 305 | /** 306 | * Run this batch process (query for the data and process the results). 307 | * 308 | * @param int $current_step Current step. 309 | */ 310 | public function run( $current_step ) { 311 | $this->current_step = $current_step; 312 | 313 | $results = $this->get_results(); 314 | 315 | if ( empty( $results ) ) { 316 | $this->update_status( 'no results found' ); 317 | return $this->format_ajax_details( array( 318 | 'success' => true, 319 | 'message' => __( 'No results found.', 'locomotive' ), 320 | ) ); 321 | } 322 | 323 | $this->process_results( $results ); 324 | 325 | $per_page = get_option( 'posts_per_page' ); 326 | if ( isset( $this->per_batch_param ) ) { 327 | $per_page = $this->args[ $this->per_batch_param ]; 328 | } 329 | 330 | /** 331 | * Filter the per_page number used to calculate total number of steps. You would get use 332 | * out of this if you had a custom $wpdb query that didn't paginate in one of the default 333 | * ways supported by the plugin. 334 | * 335 | * @param int $per_page The number of results per page. 336 | */ 337 | $per_page = apply_filters( 'loco_batch_' . $this->slug . '_per_page', $per_page ); 338 | 339 | $total_steps = ceil( $this->total_num_results / $per_page ); 340 | 341 | if ( (int) $this->current_step === (int) $total_steps ) { 342 | 343 | // The difference here calcuates the gap between the original total and the most recent query. 344 | // In the case of a deletion process the final step will have a number exactly equal to the posts_per_page. 345 | // If 20 total, then the last step would have 4 for instance. 346 | // In all other cases, the difference would be the same as the total number of results (20 - 0 = 20). 347 | // The exception is a deletion process where a new object is added during the process. 348 | // In this case, then the final step would have less then the posts_per_page but never more (so <=). 349 | // We check this difference and compare it before saying that we are finished. If not, we run the last step over. 350 | $difference = $this->total_num_results - $this->difference_in_result_totals; 351 | if ( $difference <= $per_page || $difference === $this->total_num_results ) { 352 | $this->update_status( 'finished' ); 353 | } else { 354 | $this->current_step = $this->current_step - 1; 355 | $this->update_status( 'running' ); 356 | } 357 | } else { 358 | $this->update_status( 'running' ); 359 | } 360 | 361 | $progress = ( 0 === (int) $total_steps ) ? 100 : round( ( $this->current_step / $total_steps ) * 100 ); 362 | 363 | // If there are errors, return the error variable as true so front-end can handle. 364 | if ( is_array( $this->result_errors ) && count( $this->result_errors ) > 0 ) { 365 | return $this->format_ajax_details( array( 366 | 'error' => true, 367 | 'errors' => $this->result_errors, 368 | 'total_steps' => $total_steps, 369 | 'query_results' => $results, 370 | 'progress' => $progress, 371 | ) ); 372 | } 373 | 374 | return $this->format_ajax_details( array( 375 | 'total_steps' => $total_steps, 376 | 'query_results' => $results, 377 | 'progress' => $progress, 378 | ) ); 379 | } 380 | 381 | /** 382 | * Get details for Ajax requests. 383 | * 384 | * @param array $details Array of details to send via Ajax. 385 | */ 386 | private function format_ajax_details( $details = array() ) { 387 | return wp_parse_args( $details, array( 388 | 'success' => true, 389 | 'current_step' => $this->current_step, 390 | 'callback' => $this->callback, 391 | 'status' => $this->status, 392 | 'batch' => $this->name, 393 | 'total_num_results' => $this->total_num_results, 394 | ) ); 395 | } 396 | 397 | /** 398 | * Update batch timestamps. 399 | * 400 | * @param string $status Status of batch process. 401 | */ 402 | private function update_status( $status ) { 403 | update_option( 'loco_batch_' . $this->slug, array( 404 | 'status' => $status, 405 | 'timestamp' => current_time( 'timestamp' ), 406 | ) ); 407 | 408 | $this->status = __( ucfirst( $status ) ); 409 | } 410 | 411 | /** 412 | * Loop over an array of results (posts, pages, etc) and run the callback 413 | * function that was passed through when this batch was registered. 414 | * 415 | * @param array $results Array of results from the query. 416 | */ 417 | public function process_results( $results ) { 418 | /** 419 | * The key used to define the status of whether or not a result was processed successfully. 420 | * 421 | * @param string $string_text 'success' 422 | */ 423 | $success_status = apply_filters( 'loco_batch_success_status', 'success' ); 424 | 425 | /** 426 | * The key used to define the status of whether or not a result was not able to be processed. 427 | * 428 | * @param string $string_text 'failed' 429 | */ 430 | $failed_status = apply_filters( 'loco_batch_failed_status', 'failed' ); 431 | 432 | foreach ( $results as $result ) { 433 | // If this result item has been processed already, skip it. 434 | if ( $success_status === $this->get_result_status( $result ) ) { 435 | continue; 436 | } 437 | 438 | try { 439 | call_user_func_array( $this->callback, array( $result ) ); 440 | $this->update_result_status( $result, $success_status ); 441 | } catch ( Exception $e ) { 442 | $this->update_status( $failed_status ); 443 | $this->update_result_status( $result, $failed_status ); 444 | $this->result_errors[] = array( 445 | 'item' => $result->ID, 446 | 'message' => $e->getMessage(), 447 | ); 448 | 449 | } 450 | } 451 | } 452 | 453 | /** 454 | * Update the meta info on a result. 455 | * 456 | * @param mixed $result The result we want to track meta data on. 457 | * @param string $status Status of this result in the batch. 458 | */ 459 | public function update_result_status( $result, $status ) { 460 | /** 461 | * Action to hook into when a result gets processed and it's status is updated. 462 | * 463 | * @param mixed $result The current result. 464 | * @param string $status The status to set on a result. 465 | */ 466 | do_action( 'loco_batch_' . $this->slug . '_update_result_status', $result, $status ); 467 | 468 | return $this->update_result_item_status( $result, $status ); 469 | } 470 | 471 | /** 472 | * Get the status of a result. 473 | * 474 | * @param mixed $result The result we want to get status of. 475 | */ 476 | public function get_result_status( $result ) { 477 | /** 478 | * Action to hook into when a result is being checked for whether or not 479 | * it was updated. 480 | * 481 | * @param mixed $result The current result which is getting it's status checked. 482 | */ 483 | do_action( 'loco_batch_' . $this->slug . '_get_result_status', $result ); 484 | 485 | return $this->get_result_item_status( $result ); 486 | } 487 | 488 | /** 489 | * Clear the result status for a batch. 490 | */ 491 | public function clear_result_status() { 492 | /** 493 | * Action to hook into when the 'reset' button is clicked in the admin UI. 494 | * 495 | * @param Batch $this The current batch object. 496 | */ 497 | do_action( 'loco_batch_' . $this->slug . '_clear', $this ); 498 | 499 | $this->batch_clear_result_status(); 500 | $this->update_status( 'reset' ); 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /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": "87646caa8aa0d83c8fdcc298d30d4f1d", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "doctrine/instantiator", 12 | "version": "1.4.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/doctrine/instantiator.git", 16 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", 21 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "require-dev": { 28 | "doctrine/coding-standard": "^8.0", 29 | "ext-pdo": "*", 30 | "ext-phar": "*", 31 | "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", 32 | "phpstan/phpstan": "^0.12", 33 | "phpstan/phpstan-phpunit": "^0.12", 34 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" 35 | }, 36 | "type": "library", 37 | "autoload": { 38 | "psr-4": { 39 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 40 | } 41 | }, 42 | "notification-url": "https://packagist.org/downloads/", 43 | "license": [ 44 | "MIT" 45 | ], 46 | "authors": [ 47 | { 48 | "name": "Marco Pivetta", 49 | "email": "ocramius@gmail.com", 50 | "homepage": "https://ocramius.github.io/" 51 | } 52 | ], 53 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 54 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 55 | "keywords": [ 56 | "constructor", 57 | "instantiate" 58 | ], 59 | "funding": [ 60 | { 61 | "url": "https://www.doctrine-project.org/sponsorship.html", 62 | "type": "custom" 63 | }, 64 | { 65 | "url": "https://www.patreon.com/phpdoctrine", 66 | "type": "patreon" 67 | }, 68 | { 69 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 70 | "type": "tidelift" 71 | } 72 | ], 73 | "time": "2020-11-10T18:47:58+00:00" 74 | }, 75 | { 76 | "name": "myclabs/deep-copy", 77 | "version": "1.10.2", 78 | "source": { 79 | "type": "git", 80 | "url": "https://github.com/myclabs/DeepCopy.git", 81 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" 82 | }, 83 | "dist": { 84 | "type": "zip", 85 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", 86 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", 87 | "shasum": "" 88 | }, 89 | "require": { 90 | "php": "^7.1 || ^8.0" 91 | }, 92 | "replace": { 93 | "myclabs/deep-copy": "self.version" 94 | }, 95 | "require-dev": { 96 | "doctrine/collections": "^1.0", 97 | "doctrine/common": "^2.6", 98 | "phpunit/phpunit": "^7.1" 99 | }, 100 | "type": "library", 101 | "autoload": { 102 | "psr-4": { 103 | "DeepCopy\\": "src/DeepCopy/" 104 | }, 105 | "files": [ 106 | "src/DeepCopy/deep_copy.php" 107 | ] 108 | }, 109 | "notification-url": "https://packagist.org/downloads/", 110 | "license": [ 111 | "MIT" 112 | ], 113 | "description": "Create deep copies (clones) of your objects", 114 | "keywords": [ 115 | "clone", 116 | "copy", 117 | "duplicate", 118 | "object", 119 | "object graph" 120 | ], 121 | "funding": [ 122 | { 123 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 124 | "type": "tidelift" 125 | } 126 | ], 127 | "time": "2020-11-13T09:40:50+00:00" 128 | }, 129 | { 130 | "name": "nikic/php-parser", 131 | "version": "v4.10.3", 132 | "source": { 133 | "type": "git", 134 | "url": "https://github.com/nikic/PHP-Parser.git", 135 | "reference": "dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984" 136 | }, 137 | "dist": { 138 | "type": "zip", 139 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984", 140 | "reference": "dbe56d23de8fcb157bbc0cfb3ad7c7de0cfb0984", 141 | "shasum": "" 142 | }, 143 | "require": { 144 | "ext-tokenizer": "*", 145 | "php": ">=7.0" 146 | }, 147 | "require-dev": { 148 | "ircmaxell/php-yacc": "^0.0.7", 149 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" 150 | }, 151 | "bin": [ 152 | "bin/php-parse" 153 | ], 154 | "type": "library", 155 | "extra": { 156 | "branch-alias": { 157 | "dev-master": "4.9-dev" 158 | } 159 | }, 160 | "autoload": { 161 | "psr-4": { 162 | "PhpParser\\": "lib/PhpParser" 163 | } 164 | }, 165 | "notification-url": "https://packagist.org/downloads/", 166 | "license": [ 167 | "BSD-3-Clause" 168 | ], 169 | "authors": [ 170 | { 171 | "name": "Nikita Popov" 172 | } 173 | ], 174 | "description": "A PHP parser written in PHP", 175 | "keywords": [ 176 | "parser", 177 | "php" 178 | ], 179 | "time": "2020-12-03T17:45:45+00:00" 180 | }, 181 | { 182 | "name": "phar-io/manifest", 183 | "version": "2.0.1", 184 | "source": { 185 | "type": "git", 186 | "url": "https://github.com/phar-io/manifest.git", 187 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" 188 | }, 189 | "dist": { 190 | "type": "zip", 191 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 192 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 193 | "shasum": "" 194 | }, 195 | "require": { 196 | "ext-dom": "*", 197 | "ext-phar": "*", 198 | "ext-xmlwriter": "*", 199 | "phar-io/version": "^3.0.1", 200 | "php": "^7.2 || ^8.0" 201 | }, 202 | "type": "library", 203 | "extra": { 204 | "branch-alias": { 205 | "dev-master": "2.0.x-dev" 206 | } 207 | }, 208 | "autoload": { 209 | "classmap": [ 210 | "src/" 211 | ] 212 | }, 213 | "notification-url": "https://packagist.org/downloads/", 214 | "license": [ 215 | "BSD-3-Clause" 216 | ], 217 | "authors": [ 218 | { 219 | "name": "Arne Blankerts", 220 | "email": "arne@blankerts.de", 221 | "role": "Developer" 222 | }, 223 | { 224 | "name": "Sebastian Heuer", 225 | "email": "sebastian@phpeople.de", 226 | "role": "Developer" 227 | }, 228 | { 229 | "name": "Sebastian Bergmann", 230 | "email": "sebastian@phpunit.de", 231 | "role": "Developer" 232 | } 233 | ], 234 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 235 | "time": "2020-06-27T14:33:11+00:00" 236 | }, 237 | { 238 | "name": "phar-io/version", 239 | "version": "3.0.3", 240 | "source": { 241 | "type": "git", 242 | "url": "https://github.com/phar-io/version.git", 243 | "reference": "726c026815142e4f8677b7cb7f2249c9ffb7ecae" 244 | }, 245 | "dist": { 246 | "type": "zip", 247 | "url": "https://api.github.com/repos/phar-io/version/zipball/726c026815142e4f8677b7cb7f2249c9ffb7ecae", 248 | "reference": "726c026815142e4f8677b7cb7f2249c9ffb7ecae", 249 | "shasum": "" 250 | }, 251 | "require": { 252 | "php": "^7.2 || ^8.0" 253 | }, 254 | "type": "library", 255 | "autoload": { 256 | "classmap": [ 257 | "src/" 258 | ] 259 | }, 260 | "notification-url": "https://packagist.org/downloads/", 261 | "license": [ 262 | "BSD-3-Clause" 263 | ], 264 | "authors": [ 265 | { 266 | "name": "Arne Blankerts", 267 | "email": "arne@blankerts.de", 268 | "role": "Developer" 269 | }, 270 | { 271 | "name": "Sebastian Heuer", 272 | "email": "sebastian@phpeople.de", 273 | "role": "Developer" 274 | }, 275 | { 276 | "name": "Sebastian Bergmann", 277 | "email": "sebastian@phpunit.de", 278 | "role": "Developer" 279 | } 280 | ], 281 | "description": "Library for handling version information and constraints", 282 | "time": "2020-11-30T09:21:21+00:00" 283 | }, 284 | { 285 | "name": "phpdocumentor/reflection-common", 286 | "version": "2.2.0", 287 | "source": { 288 | "type": "git", 289 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 290 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" 291 | }, 292 | "dist": { 293 | "type": "zip", 294 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", 295 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", 296 | "shasum": "" 297 | }, 298 | "require": { 299 | "php": "^7.2 || ^8.0" 300 | }, 301 | "type": "library", 302 | "extra": { 303 | "branch-alias": { 304 | "dev-2.x": "2.x-dev" 305 | } 306 | }, 307 | "autoload": { 308 | "psr-4": { 309 | "phpDocumentor\\Reflection\\": "src/" 310 | } 311 | }, 312 | "notification-url": "https://packagist.org/downloads/", 313 | "license": [ 314 | "MIT" 315 | ], 316 | "authors": [ 317 | { 318 | "name": "Jaap van Otterdijk", 319 | "email": "opensource@ijaap.nl" 320 | } 321 | ], 322 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 323 | "homepage": "http://www.phpdoc.org", 324 | "keywords": [ 325 | "FQSEN", 326 | "phpDocumentor", 327 | "phpdoc", 328 | "reflection", 329 | "static analysis" 330 | ], 331 | "time": "2020-06-27T09:03:43+00:00" 332 | }, 333 | { 334 | "name": "phpdocumentor/reflection-docblock", 335 | "version": "5.2.2", 336 | "source": { 337 | "type": "git", 338 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 339 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" 340 | }, 341 | "dist": { 342 | "type": "zip", 343 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", 344 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", 345 | "shasum": "" 346 | }, 347 | "require": { 348 | "ext-filter": "*", 349 | "php": "^7.2 || ^8.0", 350 | "phpdocumentor/reflection-common": "^2.2", 351 | "phpdocumentor/type-resolver": "^1.3", 352 | "webmozart/assert": "^1.9.1" 353 | }, 354 | "require-dev": { 355 | "mockery/mockery": "~1.3.2" 356 | }, 357 | "type": "library", 358 | "extra": { 359 | "branch-alias": { 360 | "dev-master": "5.x-dev" 361 | } 362 | }, 363 | "autoload": { 364 | "psr-4": { 365 | "phpDocumentor\\Reflection\\": "src" 366 | } 367 | }, 368 | "notification-url": "https://packagist.org/downloads/", 369 | "license": [ 370 | "MIT" 371 | ], 372 | "authors": [ 373 | { 374 | "name": "Mike van Riel", 375 | "email": "me@mikevanriel.com" 376 | }, 377 | { 378 | "name": "Jaap van Otterdijk", 379 | "email": "account@ijaap.nl" 380 | } 381 | ], 382 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 383 | "time": "2020-09-03T19:13:55+00:00" 384 | }, 385 | { 386 | "name": "phpdocumentor/type-resolver", 387 | "version": "1.4.0", 388 | "source": { 389 | "type": "git", 390 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 391 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" 392 | }, 393 | "dist": { 394 | "type": "zip", 395 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 396 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 397 | "shasum": "" 398 | }, 399 | "require": { 400 | "php": "^7.2 || ^8.0", 401 | "phpdocumentor/reflection-common": "^2.0" 402 | }, 403 | "require-dev": { 404 | "ext-tokenizer": "*" 405 | }, 406 | "type": "library", 407 | "extra": { 408 | "branch-alias": { 409 | "dev-1.x": "1.x-dev" 410 | } 411 | }, 412 | "autoload": { 413 | "psr-4": { 414 | "phpDocumentor\\Reflection\\": "src" 415 | } 416 | }, 417 | "notification-url": "https://packagist.org/downloads/", 418 | "license": [ 419 | "MIT" 420 | ], 421 | "authors": [ 422 | { 423 | "name": "Mike van Riel", 424 | "email": "me@mikevanriel.com" 425 | } 426 | ], 427 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 428 | "time": "2020-09-17T18:55:26+00:00" 429 | }, 430 | { 431 | "name": "phpspec/prophecy", 432 | "version": "1.12.1", 433 | "source": { 434 | "type": "git", 435 | "url": "https://github.com/phpspec/prophecy.git", 436 | "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" 437 | }, 438 | "dist": { 439 | "type": "zip", 440 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", 441 | "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", 442 | "shasum": "" 443 | }, 444 | "require": { 445 | "doctrine/instantiator": "^1.2", 446 | "php": "^7.2 || ~8.0, <8.1", 447 | "phpdocumentor/reflection-docblock": "^5.2", 448 | "sebastian/comparator": "^3.0 || ^4.0", 449 | "sebastian/recursion-context": "^3.0 || ^4.0" 450 | }, 451 | "require-dev": { 452 | "phpspec/phpspec": "^6.0", 453 | "phpunit/phpunit": "^8.0 || ^9.0 <9.3" 454 | }, 455 | "type": "library", 456 | "extra": { 457 | "branch-alias": { 458 | "dev-master": "1.11.x-dev" 459 | } 460 | }, 461 | "autoload": { 462 | "psr-4": { 463 | "Prophecy\\": "src/Prophecy" 464 | } 465 | }, 466 | "notification-url": "https://packagist.org/downloads/", 467 | "license": [ 468 | "MIT" 469 | ], 470 | "authors": [ 471 | { 472 | "name": "Konstantin Kudryashov", 473 | "email": "ever.zet@gmail.com", 474 | "homepage": "http://everzet.com" 475 | }, 476 | { 477 | "name": "Marcello Duarte", 478 | "email": "marcello.duarte@gmail.com" 479 | } 480 | ], 481 | "description": "Highly opinionated mocking framework for PHP 5.3+", 482 | "homepage": "https://github.com/phpspec/prophecy", 483 | "keywords": [ 484 | "Double", 485 | "Dummy", 486 | "fake", 487 | "mock", 488 | "spy", 489 | "stub" 490 | ], 491 | "time": "2020-09-29T09:10:42+00:00" 492 | }, 493 | { 494 | "name": "phpunit/php-code-coverage", 495 | "version": "9.2.5", 496 | "source": { 497 | "type": "git", 498 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 499 | "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" 500 | }, 501 | "dist": { 502 | "type": "zip", 503 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", 504 | "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", 505 | "shasum": "" 506 | }, 507 | "require": { 508 | "ext-dom": "*", 509 | "ext-libxml": "*", 510 | "ext-xmlwriter": "*", 511 | "nikic/php-parser": "^4.10.2", 512 | "php": ">=7.3", 513 | "phpunit/php-file-iterator": "^3.0.3", 514 | "phpunit/php-text-template": "^2.0.2", 515 | "sebastian/code-unit-reverse-lookup": "^2.0.2", 516 | "sebastian/complexity": "^2.0", 517 | "sebastian/environment": "^5.1.2", 518 | "sebastian/lines-of-code": "^1.0.3", 519 | "sebastian/version": "^3.0.1", 520 | "theseer/tokenizer": "^1.2.0" 521 | }, 522 | "require-dev": { 523 | "phpunit/phpunit": "^9.3" 524 | }, 525 | "suggest": { 526 | "ext-pcov": "*", 527 | "ext-xdebug": "*" 528 | }, 529 | "type": "library", 530 | "extra": { 531 | "branch-alias": { 532 | "dev-master": "9.2-dev" 533 | } 534 | }, 535 | "autoload": { 536 | "classmap": [ 537 | "src/" 538 | ] 539 | }, 540 | "notification-url": "https://packagist.org/downloads/", 541 | "license": [ 542 | "BSD-3-Clause" 543 | ], 544 | "authors": [ 545 | { 546 | "name": "Sebastian Bergmann", 547 | "email": "sebastian@phpunit.de", 548 | "role": "lead" 549 | } 550 | ], 551 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 552 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 553 | "keywords": [ 554 | "coverage", 555 | "testing", 556 | "xunit" 557 | ], 558 | "funding": [ 559 | { 560 | "url": "https://github.com/sebastianbergmann", 561 | "type": "github" 562 | } 563 | ], 564 | "time": "2020-11-28T06:44:49+00:00" 565 | }, 566 | { 567 | "name": "phpunit/php-file-iterator", 568 | "version": "3.0.5", 569 | "source": { 570 | "type": "git", 571 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 572 | "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" 573 | }, 574 | "dist": { 575 | "type": "zip", 576 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", 577 | "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", 578 | "shasum": "" 579 | }, 580 | "require": { 581 | "php": ">=7.3" 582 | }, 583 | "require-dev": { 584 | "phpunit/phpunit": "^9.3" 585 | }, 586 | "type": "library", 587 | "extra": { 588 | "branch-alias": { 589 | "dev-master": "3.0-dev" 590 | } 591 | }, 592 | "autoload": { 593 | "classmap": [ 594 | "src/" 595 | ] 596 | }, 597 | "notification-url": "https://packagist.org/downloads/", 598 | "license": [ 599 | "BSD-3-Clause" 600 | ], 601 | "authors": [ 602 | { 603 | "name": "Sebastian Bergmann", 604 | "email": "sebastian@phpunit.de", 605 | "role": "lead" 606 | } 607 | ], 608 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 609 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 610 | "keywords": [ 611 | "filesystem", 612 | "iterator" 613 | ], 614 | "funding": [ 615 | { 616 | "url": "https://github.com/sebastianbergmann", 617 | "type": "github" 618 | } 619 | ], 620 | "time": "2020-09-28T05:57:25+00:00" 621 | }, 622 | { 623 | "name": "phpunit/php-invoker", 624 | "version": "3.1.1", 625 | "source": { 626 | "type": "git", 627 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 628 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" 629 | }, 630 | "dist": { 631 | "type": "zip", 632 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 633 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 634 | "shasum": "" 635 | }, 636 | "require": { 637 | "php": ">=7.3" 638 | }, 639 | "require-dev": { 640 | "ext-pcntl": "*", 641 | "phpunit/phpunit": "^9.3" 642 | }, 643 | "suggest": { 644 | "ext-pcntl": "*" 645 | }, 646 | "type": "library", 647 | "extra": { 648 | "branch-alias": { 649 | "dev-master": "3.1-dev" 650 | } 651 | }, 652 | "autoload": { 653 | "classmap": [ 654 | "src/" 655 | ] 656 | }, 657 | "notification-url": "https://packagist.org/downloads/", 658 | "license": [ 659 | "BSD-3-Clause" 660 | ], 661 | "authors": [ 662 | { 663 | "name": "Sebastian Bergmann", 664 | "email": "sebastian@phpunit.de", 665 | "role": "lead" 666 | } 667 | ], 668 | "description": "Invoke callables with a timeout", 669 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 670 | "keywords": [ 671 | "process" 672 | ], 673 | "funding": [ 674 | { 675 | "url": "https://github.com/sebastianbergmann", 676 | "type": "github" 677 | } 678 | ], 679 | "time": "2020-09-28T05:58:55+00:00" 680 | }, 681 | { 682 | "name": "phpunit/php-text-template", 683 | "version": "2.0.4", 684 | "source": { 685 | "type": "git", 686 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 687 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" 688 | }, 689 | "dist": { 690 | "type": "zip", 691 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 692 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 693 | "shasum": "" 694 | }, 695 | "require": { 696 | "php": ">=7.3" 697 | }, 698 | "require-dev": { 699 | "phpunit/phpunit": "^9.3" 700 | }, 701 | "type": "library", 702 | "extra": { 703 | "branch-alias": { 704 | "dev-master": "2.0-dev" 705 | } 706 | }, 707 | "autoload": { 708 | "classmap": [ 709 | "src/" 710 | ] 711 | }, 712 | "notification-url": "https://packagist.org/downloads/", 713 | "license": [ 714 | "BSD-3-Clause" 715 | ], 716 | "authors": [ 717 | { 718 | "name": "Sebastian Bergmann", 719 | "email": "sebastian@phpunit.de", 720 | "role": "lead" 721 | } 722 | ], 723 | "description": "Simple template engine.", 724 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 725 | "keywords": [ 726 | "template" 727 | ], 728 | "funding": [ 729 | { 730 | "url": "https://github.com/sebastianbergmann", 731 | "type": "github" 732 | } 733 | ], 734 | "time": "2020-10-26T05:33:50+00:00" 735 | }, 736 | { 737 | "name": "phpunit/php-timer", 738 | "version": "5.0.3", 739 | "source": { 740 | "type": "git", 741 | "url": "https://github.com/sebastianbergmann/php-timer.git", 742 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" 743 | }, 744 | "dist": { 745 | "type": "zip", 746 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 747 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 748 | "shasum": "" 749 | }, 750 | "require": { 751 | "php": ">=7.3" 752 | }, 753 | "require-dev": { 754 | "phpunit/phpunit": "^9.3" 755 | }, 756 | "type": "library", 757 | "extra": { 758 | "branch-alias": { 759 | "dev-master": "5.0-dev" 760 | } 761 | }, 762 | "autoload": { 763 | "classmap": [ 764 | "src/" 765 | ] 766 | }, 767 | "notification-url": "https://packagist.org/downloads/", 768 | "license": [ 769 | "BSD-3-Clause" 770 | ], 771 | "authors": [ 772 | { 773 | "name": "Sebastian Bergmann", 774 | "email": "sebastian@phpunit.de", 775 | "role": "lead" 776 | } 777 | ], 778 | "description": "Utility class for timing", 779 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 780 | "keywords": [ 781 | "timer" 782 | ], 783 | "funding": [ 784 | { 785 | "url": "https://github.com/sebastianbergmann", 786 | "type": "github" 787 | } 788 | ], 789 | "time": "2020-10-26T13:16:10+00:00" 790 | }, 791 | { 792 | "name": "phpunit/phpunit", 793 | "version": "9.5.0", 794 | "source": { 795 | "type": "git", 796 | "url": "https://github.com/sebastianbergmann/phpunit.git", 797 | "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe" 798 | }, 799 | "dist": { 800 | "type": "zip", 801 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", 802 | "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", 803 | "shasum": "" 804 | }, 805 | "require": { 806 | "doctrine/instantiator": "^1.3.1", 807 | "ext-dom": "*", 808 | "ext-json": "*", 809 | "ext-libxml": "*", 810 | "ext-mbstring": "*", 811 | "ext-xml": "*", 812 | "ext-xmlwriter": "*", 813 | "myclabs/deep-copy": "^1.10.1", 814 | "phar-io/manifest": "^2.0.1", 815 | "phar-io/version": "^3.0.2", 816 | "php": ">=7.3", 817 | "phpspec/prophecy": "^1.12.1", 818 | "phpunit/php-code-coverage": "^9.2.3", 819 | "phpunit/php-file-iterator": "^3.0.5", 820 | "phpunit/php-invoker": "^3.1.1", 821 | "phpunit/php-text-template": "^2.0.3", 822 | "phpunit/php-timer": "^5.0.2", 823 | "sebastian/cli-parser": "^1.0.1", 824 | "sebastian/code-unit": "^1.0.6", 825 | "sebastian/comparator": "^4.0.5", 826 | "sebastian/diff": "^4.0.3", 827 | "sebastian/environment": "^5.1.3", 828 | "sebastian/exporter": "^4.0.3", 829 | "sebastian/global-state": "^5.0.1", 830 | "sebastian/object-enumerator": "^4.0.3", 831 | "sebastian/resource-operations": "^3.0.3", 832 | "sebastian/type": "^2.3", 833 | "sebastian/version": "^3.0.2" 834 | }, 835 | "require-dev": { 836 | "ext-pdo": "*", 837 | "phpspec/prophecy-phpunit": "^2.0.1" 838 | }, 839 | "suggest": { 840 | "ext-soap": "*", 841 | "ext-xdebug": "*" 842 | }, 843 | "bin": [ 844 | "phpunit" 845 | ], 846 | "type": "library", 847 | "extra": { 848 | "branch-alias": { 849 | "dev-master": "9.5-dev" 850 | } 851 | }, 852 | "autoload": { 853 | "classmap": [ 854 | "src/" 855 | ], 856 | "files": [ 857 | "src/Framework/Assert/Functions.php" 858 | ] 859 | }, 860 | "notification-url": "https://packagist.org/downloads/", 861 | "license": [ 862 | "BSD-3-Clause" 863 | ], 864 | "authors": [ 865 | { 866 | "name": "Sebastian Bergmann", 867 | "email": "sebastian@phpunit.de", 868 | "role": "lead" 869 | } 870 | ], 871 | "description": "The PHP Unit Testing framework.", 872 | "homepage": "https://phpunit.de/", 873 | "keywords": [ 874 | "phpunit", 875 | "testing", 876 | "xunit" 877 | ], 878 | "funding": [ 879 | { 880 | "url": "https://phpunit.de/donate.html", 881 | "type": "custom" 882 | }, 883 | { 884 | "url": "https://github.com/sebastianbergmann", 885 | "type": "github" 886 | } 887 | ], 888 | "time": "2020-12-04T05:05:53+00:00" 889 | }, 890 | { 891 | "name": "sebastian/cli-parser", 892 | "version": "1.0.1", 893 | "source": { 894 | "type": "git", 895 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 896 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" 897 | }, 898 | "dist": { 899 | "type": "zip", 900 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", 901 | "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", 902 | "shasum": "" 903 | }, 904 | "require": { 905 | "php": ">=7.3" 906 | }, 907 | "require-dev": { 908 | "phpunit/phpunit": "^9.3" 909 | }, 910 | "type": "library", 911 | "extra": { 912 | "branch-alias": { 913 | "dev-master": "1.0-dev" 914 | } 915 | }, 916 | "autoload": { 917 | "classmap": [ 918 | "src/" 919 | ] 920 | }, 921 | "notification-url": "https://packagist.org/downloads/", 922 | "license": [ 923 | "BSD-3-Clause" 924 | ], 925 | "authors": [ 926 | { 927 | "name": "Sebastian Bergmann", 928 | "email": "sebastian@phpunit.de", 929 | "role": "lead" 930 | } 931 | ], 932 | "description": "Library for parsing CLI options", 933 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 934 | "funding": [ 935 | { 936 | "url": "https://github.com/sebastianbergmann", 937 | "type": "github" 938 | } 939 | ], 940 | "time": "2020-09-28T06:08:49+00:00" 941 | }, 942 | { 943 | "name": "sebastian/code-unit", 944 | "version": "1.0.8", 945 | "source": { 946 | "type": "git", 947 | "url": "https://github.com/sebastianbergmann/code-unit.git", 948 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" 949 | }, 950 | "dist": { 951 | "type": "zip", 952 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", 953 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", 954 | "shasum": "" 955 | }, 956 | "require": { 957 | "php": ">=7.3" 958 | }, 959 | "require-dev": { 960 | "phpunit/phpunit": "^9.3" 961 | }, 962 | "type": "library", 963 | "extra": { 964 | "branch-alias": { 965 | "dev-master": "1.0-dev" 966 | } 967 | }, 968 | "autoload": { 969 | "classmap": [ 970 | "src/" 971 | ] 972 | }, 973 | "notification-url": "https://packagist.org/downloads/", 974 | "license": [ 975 | "BSD-3-Clause" 976 | ], 977 | "authors": [ 978 | { 979 | "name": "Sebastian Bergmann", 980 | "email": "sebastian@phpunit.de", 981 | "role": "lead" 982 | } 983 | ], 984 | "description": "Collection of value objects that represent the PHP code units", 985 | "homepage": "https://github.com/sebastianbergmann/code-unit", 986 | "funding": [ 987 | { 988 | "url": "https://github.com/sebastianbergmann", 989 | "type": "github" 990 | } 991 | ], 992 | "time": "2020-10-26T13:08:54+00:00" 993 | }, 994 | { 995 | "name": "sebastian/code-unit-reverse-lookup", 996 | "version": "2.0.3", 997 | "source": { 998 | "type": "git", 999 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1000 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" 1001 | }, 1002 | "dist": { 1003 | "type": "zip", 1004 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 1005 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 1006 | "shasum": "" 1007 | }, 1008 | "require": { 1009 | "php": ">=7.3" 1010 | }, 1011 | "require-dev": { 1012 | "phpunit/phpunit": "^9.3" 1013 | }, 1014 | "type": "library", 1015 | "extra": { 1016 | "branch-alias": { 1017 | "dev-master": "2.0-dev" 1018 | } 1019 | }, 1020 | "autoload": { 1021 | "classmap": [ 1022 | "src/" 1023 | ] 1024 | }, 1025 | "notification-url": "https://packagist.org/downloads/", 1026 | "license": [ 1027 | "BSD-3-Clause" 1028 | ], 1029 | "authors": [ 1030 | { 1031 | "name": "Sebastian Bergmann", 1032 | "email": "sebastian@phpunit.de" 1033 | } 1034 | ], 1035 | "description": "Looks up which function or method a line of code belongs to", 1036 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1037 | "funding": [ 1038 | { 1039 | "url": "https://github.com/sebastianbergmann", 1040 | "type": "github" 1041 | } 1042 | ], 1043 | "time": "2020-09-28T05:30:19+00:00" 1044 | }, 1045 | { 1046 | "name": "sebastian/comparator", 1047 | "version": "4.0.6", 1048 | "source": { 1049 | "type": "git", 1050 | "url": "https://github.com/sebastianbergmann/comparator.git", 1051 | "reference": "55f4261989e546dc112258c7a75935a81a7ce382" 1052 | }, 1053 | "dist": { 1054 | "type": "zip", 1055 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", 1056 | "reference": "55f4261989e546dc112258c7a75935a81a7ce382", 1057 | "shasum": "" 1058 | }, 1059 | "require": { 1060 | "php": ">=7.3", 1061 | "sebastian/diff": "^4.0", 1062 | "sebastian/exporter": "^4.0" 1063 | }, 1064 | "require-dev": { 1065 | "phpunit/phpunit": "^9.3" 1066 | }, 1067 | "type": "library", 1068 | "extra": { 1069 | "branch-alias": { 1070 | "dev-master": "4.0-dev" 1071 | } 1072 | }, 1073 | "autoload": { 1074 | "classmap": [ 1075 | "src/" 1076 | ] 1077 | }, 1078 | "notification-url": "https://packagist.org/downloads/", 1079 | "license": [ 1080 | "BSD-3-Clause" 1081 | ], 1082 | "authors": [ 1083 | { 1084 | "name": "Sebastian Bergmann", 1085 | "email": "sebastian@phpunit.de" 1086 | }, 1087 | { 1088 | "name": "Jeff Welch", 1089 | "email": "whatthejeff@gmail.com" 1090 | }, 1091 | { 1092 | "name": "Volker Dusch", 1093 | "email": "github@wallbash.com" 1094 | }, 1095 | { 1096 | "name": "Bernhard Schussek", 1097 | "email": "bschussek@2bepublished.at" 1098 | } 1099 | ], 1100 | "description": "Provides the functionality to compare PHP values for equality", 1101 | "homepage": "https://github.com/sebastianbergmann/comparator", 1102 | "keywords": [ 1103 | "comparator", 1104 | "compare", 1105 | "equality" 1106 | ], 1107 | "funding": [ 1108 | { 1109 | "url": "https://github.com/sebastianbergmann", 1110 | "type": "github" 1111 | } 1112 | ], 1113 | "time": "2020-10-26T15:49:45+00:00" 1114 | }, 1115 | { 1116 | "name": "sebastian/complexity", 1117 | "version": "2.0.2", 1118 | "source": { 1119 | "type": "git", 1120 | "url": "https://github.com/sebastianbergmann/complexity.git", 1121 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" 1122 | }, 1123 | "dist": { 1124 | "type": "zip", 1125 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", 1126 | "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", 1127 | "shasum": "" 1128 | }, 1129 | "require": { 1130 | "nikic/php-parser": "^4.7", 1131 | "php": ">=7.3" 1132 | }, 1133 | "require-dev": { 1134 | "phpunit/phpunit": "^9.3" 1135 | }, 1136 | "type": "library", 1137 | "extra": { 1138 | "branch-alias": { 1139 | "dev-master": "2.0-dev" 1140 | } 1141 | }, 1142 | "autoload": { 1143 | "classmap": [ 1144 | "src/" 1145 | ] 1146 | }, 1147 | "notification-url": "https://packagist.org/downloads/", 1148 | "license": [ 1149 | "BSD-3-Clause" 1150 | ], 1151 | "authors": [ 1152 | { 1153 | "name": "Sebastian Bergmann", 1154 | "email": "sebastian@phpunit.de", 1155 | "role": "lead" 1156 | } 1157 | ], 1158 | "description": "Library for calculating the complexity of PHP code units", 1159 | "homepage": "https://github.com/sebastianbergmann/complexity", 1160 | "funding": [ 1161 | { 1162 | "url": "https://github.com/sebastianbergmann", 1163 | "type": "github" 1164 | } 1165 | ], 1166 | "time": "2020-10-26T15:52:27+00:00" 1167 | }, 1168 | { 1169 | "name": "sebastian/diff", 1170 | "version": "4.0.4", 1171 | "source": { 1172 | "type": "git", 1173 | "url": "https://github.com/sebastianbergmann/diff.git", 1174 | "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" 1175 | }, 1176 | "dist": { 1177 | "type": "zip", 1178 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", 1179 | "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", 1180 | "shasum": "" 1181 | }, 1182 | "require": { 1183 | "php": ">=7.3" 1184 | }, 1185 | "require-dev": { 1186 | "phpunit/phpunit": "^9.3", 1187 | "symfony/process": "^4.2 || ^5" 1188 | }, 1189 | "type": "library", 1190 | "extra": { 1191 | "branch-alias": { 1192 | "dev-master": "4.0-dev" 1193 | } 1194 | }, 1195 | "autoload": { 1196 | "classmap": [ 1197 | "src/" 1198 | ] 1199 | }, 1200 | "notification-url": "https://packagist.org/downloads/", 1201 | "license": [ 1202 | "BSD-3-Clause" 1203 | ], 1204 | "authors": [ 1205 | { 1206 | "name": "Sebastian Bergmann", 1207 | "email": "sebastian@phpunit.de" 1208 | }, 1209 | { 1210 | "name": "Kore Nordmann", 1211 | "email": "mail@kore-nordmann.de" 1212 | } 1213 | ], 1214 | "description": "Diff implementation", 1215 | "homepage": "https://github.com/sebastianbergmann/diff", 1216 | "keywords": [ 1217 | "diff", 1218 | "udiff", 1219 | "unidiff", 1220 | "unified diff" 1221 | ], 1222 | "funding": [ 1223 | { 1224 | "url": "https://github.com/sebastianbergmann", 1225 | "type": "github" 1226 | } 1227 | ], 1228 | "time": "2020-10-26T13:10:38+00:00" 1229 | }, 1230 | { 1231 | "name": "sebastian/environment", 1232 | "version": "5.1.3", 1233 | "source": { 1234 | "type": "git", 1235 | "url": "https://github.com/sebastianbergmann/environment.git", 1236 | "reference": "388b6ced16caa751030f6a69e588299fa09200ac" 1237 | }, 1238 | "dist": { 1239 | "type": "zip", 1240 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", 1241 | "reference": "388b6ced16caa751030f6a69e588299fa09200ac", 1242 | "shasum": "" 1243 | }, 1244 | "require": { 1245 | "php": ">=7.3" 1246 | }, 1247 | "require-dev": { 1248 | "phpunit/phpunit": "^9.3" 1249 | }, 1250 | "suggest": { 1251 | "ext-posix": "*" 1252 | }, 1253 | "type": "library", 1254 | "extra": { 1255 | "branch-alias": { 1256 | "dev-master": "5.1-dev" 1257 | } 1258 | }, 1259 | "autoload": { 1260 | "classmap": [ 1261 | "src/" 1262 | ] 1263 | }, 1264 | "notification-url": "https://packagist.org/downloads/", 1265 | "license": [ 1266 | "BSD-3-Clause" 1267 | ], 1268 | "authors": [ 1269 | { 1270 | "name": "Sebastian Bergmann", 1271 | "email": "sebastian@phpunit.de" 1272 | } 1273 | ], 1274 | "description": "Provides functionality to handle HHVM/PHP environments", 1275 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1276 | "keywords": [ 1277 | "Xdebug", 1278 | "environment", 1279 | "hhvm" 1280 | ], 1281 | "funding": [ 1282 | { 1283 | "url": "https://github.com/sebastianbergmann", 1284 | "type": "github" 1285 | } 1286 | ], 1287 | "time": "2020-09-28T05:52:38+00:00" 1288 | }, 1289 | { 1290 | "name": "sebastian/exporter", 1291 | "version": "4.0.3", 1292 | "source": { 1293 | "type": "git", 1294 | "url": "https://github.com/sebastianbergmann/exporter.git", 1295 | "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" 1296 | }, 1297 | "dist": { 1298 | "type": "zip", 1299 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", 1300 | "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", 1301 | "shasum": "" 1302 | }, 1303 | "require": { 1304 | "php": ">=7.3", 1305 | "sebastian/recursion-context": "^4.0" 1306 | }, 1307 | "require-dev": { 1308 | "ext-mbstring": "*", 1309 | "phpunit/phpunit": "^9.3" 1310 | }, 1311 | "type": "library", 1312 | "extra": { 1313 | "branch-alias": { 1314 | "dev-master": "4.0-dev" 1315 | } 1316 | }, 1317 | "autoload": { 1318 | "classmap": [ 1319 | "src/" 1320 | ] 1321 | }, 1322 | "notification-url": "https://packagist.org/downloads/", 1323 | "license": [ 1324 | "BSD-3-Clause" 1325 | ], 1326 | "authors": [ 1327 | { 1328 | "name": "Sebastian Bergmann", 1329 | "email": "sebastian@phpunit.de" 1330 | }, 1331 | { 1332 | "name": "Jeff Welch", 1333 | "email": "whatthejeff@gmail.com" 1334 | }, 1335 | { 1336 | "name": "Volker Dusch", 1337 | "email": "github@wallbash.com" 1338 | }, 1339 | { 1340 | "name": "Adam Harvey", 1341 | "email": "aharvey@php.net" 1342 | }, 1343 | { 1344 | "name": "Bernhard Schussek", 1345 | "email": "bschussek@gmail.com" 1346 | } 1347 | ], 1348 | "description": "Provides the functionality to export PHP variables for visualization", 1349 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1350 | "keywords": [ 1351 | "export", 1352 | "exporter" 1353 | ], 1354 | "funding": [ 1355 | { 1356 | "url": "https://github.com/sebastianbergmann", 1357 | "type": "github" 1358 | } 1359 | ], 1360 | "time": "2020-09-28T05:24:23+00:00" 1361 | }, 1362 | { 1363 | "name": "sebastian/global-state", 1364 | "version": "5.0.2", 1365 | "source": { 1366 | "type": "git", 1367 | "url": "https://github.com/sebastianbergmann/global-state.git", 1368 | "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" 1369 | }, 1370 | "dist": { 1371 | "type": "zip", 1372 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", 1373 | "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", 1374 | "shasum": "" 1375 | }, 1376 | "require": { 1377 | "php": ">=7.3", 1378 | "sebastian/object-reflector": "^2.0", 1379 | "sebastian/recursion-context": "^4.0" 1380 | }, 1381 | "require-dev": { 1382 | "ext-dom": "*", 1383 | "phpunit/phpunit": "^9.3" 1384 | }, 1385 | "suggest": { 1386 | "ext-uopz": "*" 1387 | }, 1388 | "type": "library", 1389 | "extra": { 1390 | "branch-alias": { 1391 | "dev-master": "5.0-dev" 1392 | } 1393 | }, 1394 | "autoload": { 1395 | "classmap": [ 1396 | "src/" 1397 | ] 1398 | }, 1399 | "notification-url": "https://packagist.org/downloads/", 1400 | "license": [ 1401 | "BSD-3-Clause" 1402 | ], 1403 | "authors": [ 1404 | { 1405 | "name": "Sebastian Bergmann", 1406 | "email": "sebastian@phpunit.de" 1407 | } 1408 | ], 1409 | "description": "Snapshotting of global state", 1410 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1411 | "keywords": [ 1412 | "global state" 1413 | ], 1414 | "funding": [ 1415 | { 1416 | "url": "https://github.com/sebastianbergmann", 1417 | "type": "github" 1418 | } 1419 | ], 1420 | "time": "2020-10-26T15:55:19+00:00" 1421 | }, 1422 | { 1423 | "name": "sebastian/lines-of-code", 1424 | "version": "1.0.3", 1425 | "source": { 1426 | "type": "git", 1427 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1428 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" 1429 | }, 1430 | "dist": { 1431 | "type": "zip", 1432 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1433 | "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", 1434 | "shasum": "" 1435 | }, 1436 | "require": { 1437 | "nikic/php-parser": "^4.6", 1438 | "php": ">=7.3" 1439 | }, 1440 | "require-dev": { 1441 | "phpunit/phpunit": "^9.3" 1442 | }, 1443 | "type": "library", 1444 | "extra": { 1445 | "branch-alias": { 1446 | "dev-master": "1.0-dev" 1447 | } 1448 | }, 1449 | "autoload": { 1450 | "classmap": [ 1451 | "src/" 1452 | ] 1453 | }, 1454 | "notification-url": "https://packagist.org/downloads/", 1455 | "license": [ 1456 | "BSD-3-Clause" 1457 | ], 1458 | "authors": [ 1459 | { 1460 | "name": "Sebastian Bergmann", 1461 | "email": "sebastian@phpunit.de", 1462 | "role": "lead" 1463 | } 1464 | ], 1465 | "description": "Library for counting the lines of code in PHP source code", 1466 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1467 | "funding": [ 1468 | { 1469 | "url": "https://github.com/sebastianbergmann", 1470 | "type": "github" 1471 | } 1472 | ], 1473 | "time": "2020-11-28T06:42:11+00:00" 1474 | }, 1475 | { 1476 | "name": "sebastian/object-enumerator", 1477 | "version": "4.0.4", 1478 | "source": { 1479 | "type": "git", 1480 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1481 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" 1482 | }, 1483 | "dist": { 1484 | "type": "zip", 1485 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", 1486 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", 1487 | "shasum": "" 1488 | }, 1489 | "require": { 1490 | "php": ">=7.3", 1491 | "sebastian/object-reflector": "^2.0", 1492 | "sebastian/recursion-context": "^4.0" 1493 | }, 1494 | "require-dev": { 1495 | "phpunit/phpunit": "^9.3" 1496 | }, 1497 | "type": "library", 1498 | "extra": { 1499 | "branch-alias": { 1500 | "dev-master": "4.0-dev" 1501 | } 1502 | }, 1503 | "autoload": { 1504 | "classmap": [ 1505 | "src/" 1506 | ] 1507 | }, 1508 | "notification-url": "https://packagist.org/downloads/", 1509 | "license": [ 1510 | "BSD-3-Clause" 1511 | ], 1512 | "authors": [ 1513 | { 1514 | "name": "Sebastian Bergmann", 1515 | "email": "sebastian@phpunit.de" 1516 | } 1517 | ], 1518 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1519 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1520 | "funding": [ 1521 | { 1522 | "url": "https://github.com/sebastianbergmann", 1523 | "type": "github" 1524 | } 1525 | ], 1526 | "time": "2020-10-26T13:12:34+00:00" 1527 | }, 1528 | { 1529 | "name": "sebastian/object-reflector", 1530 | "version": "2.0.4", 1531 | "source": { 1532 | "type": "git", 1533 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1534 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" 1535 | }, 1536 | "dist": { 1537 | "type": "zip", 1538 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1539 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1540 | "shasum": "" 1541 | }, 1542 | "require": { 1543 | "php": ">=7.3" 1544 | }, 1545 | "require-dev": { 1546 | "phpunit/phpunit": "^9.3" 1547 | }, 1548 | "type": "library", 1549 | "extra": { 1550 | "branch-alias": { 1551 | "dev-master": "2.0-dev" 1552 | } 1553 | }, 1554 | "autoload": { 1555 | "classmap": [ 1556 | "src/" 1557 | ] 1558 | }, 1559 | "notification-url": "https://packagist.org/downloads/", 1560 | "license": [ 1561 | "BSD-3-Clause" 1562 | ], 1563 | "authors": [ 1564 | { 1565 | "name": "Sebastian Bergmann", 1566 | "email": "sebastian@phpunit.de" 1567 | } 1568 | ], 1569 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1570 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1571 | "funding": [ 1572 | { 1573 | "url": "https://github.com/sebastianbergmann", 1574 | "type": "github" 1575 | } 1576 | ], 1577 | "time": "2020-10-26T13:14:26+00:00" 1578 | }, 1579 | { 1580 | "name": "sebastian/recursion-context", 1581 | "version": "4.0.4", 1582 | "source": { 1583 | "type": "git", 1584 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1585 | "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" 1586 | }, 1587 | "dist": { 1588 | "type": "zip", 1589 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", 1590 | "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", 1591 | "shasum": "" 1592 | }, 1593 | "require": { 1594 | "php": ">=7.3" 1595 | }, 1596 | "require-dev": { 1597 | "phpunit/phpunit": "^9.3" 1598 | }, 1599 | "type": "library", 1600 | "extra": { 1601 | "branch-alias": { 1602 | "dev-master": "4.0-dev" 1603 | } 1604 | }, 1605 | "autoload": { 1606 | "classmap": [ 1607 | "src/" 1608 | ] 1609 | }, 1610 | "notification-url": "https://packagist.org/downloads/", 1611 | "license": [ 1612 | "BSD-3-Clause" 1613 | ], 1614 | "authors": [ 1615 | { 1616 | "name": "Sebastian Bergmann", 1617 | "email": "sebastian@phpunit.de" 1618 | }, 1619 | { 1620 | "name": "Jeff Welch", 1621 | "email": "whatthejeff@gmail.com" 1622 | }, 1623 | { 1624 | "name": "Adam Harvey", 1625 | "email": "aharvey@php.net" 1626 | } 1627 | ], 1628 | "description": "Provides functionality to recursively process PHP variables", 1629 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1630 | "funding": [ 1631 | { 1632 | "url": "https://github.com/sebastianbergmann", 1633 | "type": "github" 1634 | } 1635 | ], 1636 | "time": "2020-10-26T13:17:30+00:00" 1637 | }, 1638 | { 1639 | "name": "sebastian/resource-operations", 1640 | "version": "3.0.3", 1641 | "source": { 1642 | "type": "git", 1643 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1644 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" 1645 | }, 1646 | "dist": { 1647 | "type": "zip", 1648 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1649 | "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", 1650 | "shasum": "" 1651 | }, 1652 | "require": { 1653 | "php": ">=7.3" 1654 | }, 1655 | "require-dev": { 1656 | "phpunit/phpunit": "^9.0" 1657 | }, 1658 | "type": "library", 1659 | "extra": { 1660 | "branch-alias": { 1661 | "dev-master": "3.0-dev" 1662 | } 1663 | }, 1664 | "autoload": { 1665 | "classmap": [ 1666 | "src/" 1667 | ] 1668 | }, 1669 | "notification-url": "https://packagist.org/downloads/", 1670 | "license": [ 1671 | "BSD-3-Clause" 1672 | ], 1673 | "authors": [ 1674 | { 1675 | "name": "Sebastian Bergmann", 1676 | "email": "sebastian@phpunit.de" 1677 | } 1678 | ], 1679 | "description": "Provides a list of PHP built-in functions that operate on resources", 1680 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1681 | "funding": [ 1682 | { 1683 | "url": "https://github.com/sebastianbergmann", 1684 | "type": "github" 1685 | } 1686 | ], 1687 | "time": "2020-09-28T06:45:17+00:00" 1688 | }, 1689 | { 1690 | "name": "sebastian/type", 1691 | "version": "2.3.1", 1692 | "source": { 1693 | "type": "git", 1694 | "url": "https://github.com/sebastianbergmann/type.git", 1695 | "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" 1696 | }, 1697 | "dist": { 1698 | "type": "zip", 1699 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", 1700 | "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", 1701 | "shasum": "" 1702 | }, 1703 | "require": { 1704 | "php": ">=7.3" 1705 | }, 1706 | "require-dev": { 1707 | "phpunit/phpunit": "^9.3" 1708 | }, 1709 | "type": "library", 1710 | "extra": { 1711 | "branch-alias": { 1712 | "dev-master": "2.3-dev" 1713 | } 1714 | }, 1715 | "autoload": { 1716 | "classmap": [ 1717 | "src/" 1718 | ] 1719 | }, 1720 | "notification-url": "https://packagist.org/downloads/", 1721 | "license": [ 1722 | "BSD-3-Clause" 1723 | ], 1724 | "authors": [ 1725 | { 1726 | "name": "Sebastian Bergmann", 1727 | "email": "sebastian@phpunit.de", 1728 | "role": "lead" 1729 | } 1730 | ], 1731 | "description": "Collection of value objects that represent the types of the PHP type system", 1732 | "homepage": "https://github.com/sebastianbergmann/type", 1733 | "funding": [ 1734 | { 1735 | "url": "https://github.com/sebastianbergmann", 1736 | "type": "github" 1737 | } 1738 | ], 1739 | "time": "2020-10-26T13:18:59+00:00" 1740 | }, 1741 | { 1742 | "name": "sebastian/version", 1743 | "version": "3.0.2", 1744 | "source": { 1745 | "type": "git", 1746 | "url": "https://github.com/sebastianbergmann/version.git", 1747 | "reference": "c6c1022351a901512170118436c764e473f6de8c" 1748 | }, 1749 | "dist": { 1750 | "type": "zip", 1751 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", 1752 | "reference": "c6c1022351a901512170118436c764e473f6de8c", 1753 | "shasum": "" 1754 | }, 1755 | "require": { 1756 | "php": ">=7.3" 1757 | }, 1758 | "type": "library", 1759 | "extra": { 1760 | "branch-alias": { 1761 | "dev-master": "3.0-dev" 1762 | } 1763 | }, 1764 | "autoload": { 1765 | "classmap": [ 1766 | "src/" 1767 | ] 1768 | }, 1769 | "notification-url": "https://packagist.org/downloads/", 1770 | "license": [ 1771 | "BSD-3-Clause" 1772 | ], 1773 | "authors": [ 1774 | { 1775 | "name": "Sebastian Bergmann", 1776 | "email": "sebastian@phpunit.de", 1777 | "role": "lead" 1778 | } 1779 | ], 1780 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1781 | "homepage": "https://github.com/sebastianbergmann/version", 1782 | "funding": [ 1783 | { 1784 | "url": "https://github.com/sebastianbergmann", 1785 | "type": "github" 1786 | } 1787 | ], 1788 | "time": "2020-09-28T06:39:44+00:00" 1789 | }, 1790 | { 1791 | "name": "squizlabs/php_codesniffer", 1792 | "version": "3.5.8", 1793 | "source": { 1794 | "type": "git", 1795 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1796 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" 1797 | }, 1798 | "dist": { 1799 | "type": "zip", 1800 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", 1801 | "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", 1802 | "shasum": "" 1803 | }, 1804 | "require": { 1805 | "ext-simplexml": "*", 1806 | "ext-tokenizer": "*", 1807 | "ext-xmlwriter": "*", 1808 | "php": ">=5.4.0" 1809 | }, 1810 | "require-dev": { 1811 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1812 | }, 1813 | "bin": [ 1814 | "bin/phpcs", 1815 | "bin/phpcbf" 1816 | ], 1817 | "type": "library", 1818 | "extra": { 1819 | "branch-alias": { 1820 | "dev-master": "3.x-dev" 1821 | } 1822 | }, 1823 | "notification-url": "https://packagist.org/downloads/", 1824 | "license": [ 1825 | "BSD-3-Clause" 1826 | ], 1827 | "authors": [ 1828 | { 1829 | "name": "Greg Sherwood", 1830 | "role": "lead" 1831 | } 1832 | ], 1833 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 1834 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 1835 | "keywords": [ 1836 | "phpcs", 1837 | "standards" 1838 | ], 1839 | "time": "2020-10-23T02:01:07+00:00" 1840 | }, 1841 | { 1842 | "name": "symfony/polyfill-ctype", 1843 | "version": "v1.20.0", 1844 | "source": { 1845 | "type": "git", 1846 | "url": "https://github.com/symfony/polyfill-ctype.git", 1847 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" 1848 | }, 1849 | "dist": { 1850 | "type": "zip", 1851 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", 1852 | "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", 1853 | "shasum": "" 1854 | }, 1855 | "require": { 1856 | "php": ">=7.1" 1857 | }, 1858 | "suggest": { 1859 | "ext-ctype": "For best performance" 1860 | }, 1861 | "type": "library", 1862 | "extra": { 1863 | "branch-alias": { 1864 | "dev-main": "1.20-dev" 1865 | }, 1866 | "thanks": { 1867 | "name": "symfony/polyfill", 1868 | "url": "https://github.com/symfony/polyfill" 1869 | } 1870 | }, 1871 | "autoload": { 1872 | "psr-4": { 1873 | "Symfony\\Polyfill\\Ctype\\": "" 1874 | }, 1875 | "files": [ 1876 | "bootstrap.php" 1877 | ] 1878 | }, 1879 | "notification-url": "https://packagist.org/downloads/", 1880 | "license": [ 1881 | "MIT" 1882 | ], 1883 | "authors": [ 1884 | { 1885 | "name": "Gert de Pagter", 1886 | "email": "BackEndTea@gmail.com" 1887 | }, 1888 | { 1889 | "name": "Symfony Community", 1890 | "homepage": "https://symfony.com/contributors" 1891 | } 1892 | ], 1893 | "description": "Symfony polyfill for ctype functions", 1894 | "homepage": "https://symfony.com", 1895 | "keywords": [ 1896 | "compatibility", 1897 | "ctype", 1898 | "polyfill", 1899 | "portable" 1900 | ], 1901 | "funding": [ 1902 | { 1903 | "url": "https://symfony.com/sponsor", 1904 | "type": "custom" 1905 | }, 1906 | { 1907 | "url": "https://github.com/fabpot", 1908 | "type": "github" 1909 | }, 1910 | { 1911 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1912 | "type": "tidelift" 1913 | } 1914 | ], 1915 | "time": "2020-10-23T14:02:19+00:00" 1916 | }, 1917 | { 1918 | "name": "theseer/tokenizer", 1919 | "version": "1.2.0", 1920 | "source": { 1921 | "type": "git", 1922 | "url": "https://github.com/theseer/tokenizer.git", 1923 | "reference": "75a63c33a8577608444246075ea0af0d052e452a" 1924 | }, 1925 | "dist": { 1926 | "type": "zip", 1927 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", 1928 | "reference": "75a63c33a8577608444246075ea0af0d052e452a", 1929 | "shasum": "" 1930 | }, 1931 | "require": { 1932 | "ext-dom": "*", 1933 | "ext-tokenizer": "*", 1934 | "ext-xmlwriter": "*", 1935 | "php": "^7.2 || ^8.0" 1936 | }, 1937 | "type": "library", 1938 | "autoload": { 1939 | "classmap": [ 1940 | "src/" 1941 | ] 1942 | }, 1943 | "notification-url": "https://packagist.org/downloads/", 1944 | "license": [ 1945 | "BSD-3-Clause" 1946 | ], 1947 | "authors": [ 1948 | { 1949 | "name": "Arne Blankerts", 1950 | "email": "arne@blankerts.de", 1951 | "role": "Developer" 1952 | } 1953 | ], 1954 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1955 | "funding": [ 1956 | { 1957 | "url": "https://github.com/theseer", 1958 | "type": "github" 1959 | } 1960 | ], 1961 | "time": "2020-07-12T23:59:07+00:00" 1962 | }, 1963 | { 1964 | "name": "webmozart/assert", 1965 | "version": "1.9.1", 1966 | "source": { 1967 | "type": "git", 1968 | "url": "https://github.com/webmozart/assert.git", 1969 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" 1970 | }, 1971 | "dist": { 1972 | "type": "zip", 1973 | "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", 1974 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", 1975 | "shasum": "" 1976 | }, 1977 | "require": { 1978 | "php": "^5.3.3 || ^7.0 || ^8.0", 1979 | "symfony/polyfill-ctype": "^1.8" 1980 | }, 1981 | "conflict": { 1982 | "phpstan/phpstan": "<0.12.20", 1983 | "vimeo/psalm": "<3.9.1" 1984 | }, 1985 | "require-dev": { 1986 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1987 | }, 1988 | "type": "library", 1989 | "autoload": { 1990 | "psr-4": { 1991 | "Webmozart\\Assert\\": "src/" 1992 | } 1993 | }, 1994 | "notification-url": "https://packagist.org/downloads/", 1995 | "license": [ 1996 | "MIT" 1997 | ], 1998 | "authors": [ 1999 | { 2000 | "name": "Bernhard Schussek", 2001 | "email": "bschussek@gmail.com" 2002 | } 2003 | ], 2004 | "description": "Assertions to validate method input/output with nice error messages.", 2005 | "keywords": [ 2006 | "assert", 2007 | "check", 2008 | "validate" 2009 | ], 2010 | "time": "2020-07-08T17:02:28+00:00" 2011 | }, 2012 | { 2013 | "name": "wp-coding-standards/wpcs", 2014 | "version": "2.3.0", 2015 | "source": { 2016 | "type": "git", 2017 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 2018 | "reference": "7da1894633f168fe244afc6de00d141f27517b62" 2019 | }, 2020 | "dist": { 2021 | "type": "zip", 2022 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", 2023 | "reference": "7da1894633f168fe244afc6de00d141f27517b62", 2024 | "shasum": "" 2025 | }, 2026 | "require": { 2027 | "php": ">=5.4", 2028 | "squizlabs/php_codesniffer": "^3.3.1" 2029 | }, 2030 | "require-dev": { 2031 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", 2032 | "phpcompatibility/php-compatibility": "^9.0", 2033 | "phpcsstandards/phpcsdevtools": "^1.0", 2034 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 2035 | }, 2036 | "suggest": { 2037 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 2038 | }, 2039 | "type": "phpcodesniffer-standard", 2040 | "notification-url": "https://packagist.org/downloads/", 2041 | "license": [ 2042 | "MIT" 2043 | ], 2044 | "authors": [ 2045 | { 2046 | "name": "Contributors", 2047 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 2048 | } 2049 | ], 2050 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 2051 | "keywords": [ 2052 | "phpcs", 2053 | "standards", 2054 | "wordpress" 2055 | ], 2056 | "time": "2020-05-13T23:57:56+00:00" 2057 | } 2058 | ], 2059 | "aliases": [], 2060 | "minimum-stability": "stable", 2061 | "stability-flags": [], 2062 | "prefer-stable": false, 2063 | "prefer-lowest": false, 2064 | "platform": [], 2065 | "platform-dev": [], 2066 | "plugin-api-version": "1.1.0" 2067 | } 2068 | --------------------------------------------------------------------------------