diff --git a/.gitignore b/.gitignore index bfbfcaa..22fa3f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -/tests/coverage -/vendor -composer.lock +tests/coverage +vendor diff --git a/.travis.yml b/.travis.yml index 60337b8..211f928 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ +language: php sudo: false dist: trusty -language: php - notifications: email: false @@ -11,48 +10,34 @@ cache: - $HOME/.composer/cache matrix: + fast_finish: true include: - php: 7.2 - env: WP_VERSION=trunk + env: WP_VERSION=trunk WP_MULTISITE=0 RUN_PHPCS=1 + - php: 7.2 + env: WP_VERSION=trunk WP_MULTISITE=1 - php: 7.1 - env: WP_VERSION=latest + env: WP_VERSION=latest WP_MULTISITE=0 - php: 7.0 - env: WP_VERSION=latest - - php: 5.6 - env: WP_VERSION=latest - #- php: 5.6 - # env: WP_TRAVISCI=phpcs - - php: 5.3 - env: WP_VERSION=latest - dist: precise + env: WP_VERSION=latest WP_MULTISITE=0 + + # The following WooCommerce core test currently fails in multisite, with or without this + # plugin being active: + # + # WC_Tests_Setup_Functions::test_wizard_in_cart_payment_gateways() + allow_failures: + - php: 7.2 + env: WP_VERSION=trunk WP_MULTISITE=1 + before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" - - | - if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then - phpenv config-rm xdebug.ini - else - echo "xdebug.ini does not exist" - fi - - | - if [[ ! -z "$WP_VERSION" ]] ; then - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - composer global require "phpunit/phpunit=4.8.*|5.7.*" - fi - - | - if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then - composer global require wp-coding-standards/wpcs - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs - fi + - bash tests/bin/install-wp-tests.sh woocommerce_test root '' localhost $WP_VERSION - composer install --prefer-source script: + - phpunit - | - if [[ ! -z "$WP_VERSION" ]] ; then - phpunit - WP_MULTISITE=1 phpunit - fi - - | - if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then - phpcs + if [[ ${RUN_PHPCS} == 1 ]]; then + ./vendor/bin/phpcs fi diff --git a/composer.json b/composer.json index 3b2a7fd..35017c6 100644 --- a/composer.json +++ b/composer.json @@ -8,11 +8,23 @@ "composer/installers": "^1.4", "xrstf/composer-php52": "^1.0" }, + "require-dev": { + "php": "^7.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "wimg/php-compatibility": "^8.1", + "woocommerce/woocommerce": "dev-master", + "woocommerce/woocommerce-sniffs": "^0.0.1", + "wp-coding-standards/wpcs": "^0.14" + }, "autoload": { - "classmap": ["includes"] + "classmap": [ + "includes" + ] }, "autoload-dev": { - "classmap": ["tests/test-tools"] + "classmap": [ + "vendor/woocommerce/woocommerce/tests/framework" + ] }, "scripts": { "post-install-cmd": [ @@ -25,7 +37,14 @@ "xrstf\\Composer52\\Generator::onPostInstallCmd" ], "test-coverage": [ - "phpunit --coverage-html=tests/coverage" + "phpunit --testsuite=plugin --coverage-html=tests/coverage" ] + }, + "extra": { + "installer-paths": { + "vendor/{$vendor}/{$name}": [ + "woocommerce/woocommerce" + ] + } } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..5ac7941 --- /dev/null +++ b/composer.lock @@ -0,0 +1,470 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "fd86f99767fa5412fdd10e6971be6a34", + "packages": [ + { + "name": "composer/installers", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "049797d727261bf27f2690430d935067710049c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", + "reference": "049797d727261bf27f2690430d935067710049c2", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.0.*@dev", + "phpunit/phpunit": "^4.8.36" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Thelia", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "modulework", + "modx", + "moodle", + "osclass", + "phpbb", + "piwik", + "ppi", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "symfony", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "time": "2017-12-29T09:13:20+00:00" + }, + { + "name": "xrstf/composer-php52", + "version": "v1.0.20", + "source": { + "type": "git", + "url": "https://github.com/composer-php52/composer-php52.git", + "reference": "bd41459d5e27df8d33057842b32377c39e97a5a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer-php52/composer-php52/zipball/bd41459d5e27df8d33057842b32377c39e97a5a8", + "reference": "bd41459d5e27df8d33057842b32377c39e97a5a8", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.x-dev" + } + }, + "autoload": { + "psr-0": { + "xrstf\\Composer52": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "time": "2016-04-16T21:52:24+00:00" + } + ], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.4.4", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0", + "php": "^5.3|^7", + "squizlabs/php_codesniffer": "*" + }, + "require-dev": { + "composer/composer": "*", + "wimg/php-compatibility": "^8.0" + }, + "suggest": { + "dealerdirect/qa-tools": "All the PHP QA tools you'll need" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "f.nijhof@dealerdirect.nl", + "homepage": "http://workingatdealerdirect.eu", + "role": "Developer" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://workingatdealerdirect.eu", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "time": "2017-12-06T16:27:17+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2017-12-19T21:44:46+00:00" + }, + { + "name": "wimg/php-compatibility", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/wimg/PHPCompatibility.git", + "reference": "4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wimg/PHPCompatibility/zipball/4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e", + "reference": "4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.2 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "PHPCompatibility\\": "PHPCompatibility/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "time": "2017-12-27T21:58:38+00:00" + }, + { + "name": "woocommerce/woocommerce", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git", + "reference": "a887b49bb489852280501d1774dc05058bb47b7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/a887b49bb489852280501d1774dc05058bb47b7d", + "reference": "a887b49bb489852280501d1774dc05058bb47b7d", + "shasum": "" + }, + "require": { + "composer/installers": "~1.2" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3", + "phpunit/phpunit": "6.2.3", + "squizlabs/php_codesniffer": "*", + "wimg/php-compatibility": "^8.0", + "woocommerce/woocommerce-git-hooks": "*", + "woocommerce/woocommerce-sniffs": "*", + "wp-coding-standards/wpcs": "^0.14" + }, + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", + "homepage": "https://woocommerce.com/", + "time": "2018-01-11T14:34:41+00:00" + }, + { + "name": "woocommerce/woocommerce-sniffs", + "version": "0.0.1", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-sniffs.git", + "reference": "383d5b361c1d7532ae1ca6156fd7619fd37bbc05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/383d5b361c1d7532ae1ca6156fd7619fd37bbc05", + "reference": "383d5b361c1d7532ae1ca6156fd7619fd37bbc05", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Claudio Sanches", + "email": "claudio@automattic.com" + } + ], + "description": "WooCommerce sniffs", + "keywords": [ + "phpcs", + "standards", + "woocommerce", + "wordpress" + ], + "time": "2017-12-21T22:52:52+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "0.14.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git", + "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8cadf48fa1c70b2381988e0a79e029e011a8f41c", + "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "time": "2017-11-01T15:10:46+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "woocommerce/woocommerce": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.2" + }, + "platform-dev": { + "php": "^7.0" + } +} diff --git a/includes/class-wc-custom-order-table-install.php b/includes/class-wc-custom-order-table-install.php index 09c3882..03d37c9 100644 --- a/includes/class-wc-custom-order-table-install.php +++ b/includes/class-wc-custom-order-table-install.php @@ -57,6 +57,7 @@ protected static function install_tables() { order_id BIGINT UNSIGNED NOT NULL, order_key varchar(100) NOT NULL, customer_id BIGINT UNSIGNED NOT NULL, + billing_index varchar(255) NOT NULL, billing_first_name varchar(100) NOT NULL, billing_last_name varchar(100) NOT NULL, billing_company varchar(100) NOT NULL, @@ -68,6 +69,7 @@ protected static function install_tables() { billing_country varchar(100) NOT NULL, billing_email varchar(200) NOT NULL, billing_phone varchar(200) NOT NULL, + shipping_index varchar(255) NOT NULL, shipping_first_name varchar(100) NOT NULL, shipping_last_name varchar(100) NOT NULL, shipping_company varchar(100) NOT NULL, @@ -79,21 +81,21 @@ protected static function install_tables() { shipping_country varchar(100) NOT NULL, payment_method varchar(100) NOT NULL, payment_method_title varchar(100) NOT NULL, - discount_total float NOT NULL DEFAULT 0, - discount_tax float NOT NULL DEFAULT 0, - shipping_total float NOT NULL DEFAULT 0, - shipping_tax float NOT NULL DEFAULT 0, - cart_tax float NOT NULL DEFAULT 0, - total float NOT NULL DEFAULT 0, + discount_total varchar(100) NOT NULL DEFAULT 0, + discount_tax varchar(100) NOT NULL DEFAULT 0, + shipping_total varchar(100) NOT NULL DEFAULT 0, + shipping_tax varchar(100) NOT NULL DEFAULT 0, + cart_tax varchar(100) NOT NULL DEFAULT 0, + total varchar(100) NOT NULL DEFAULT 0, version varchar(16) NOT NULL, currency varchar(3) NOT NULL, - prices_include_tax tinyint(1) NOT NULL, + prices_include_tax varchar(3) NOT NULL, transaction_id varchar(200) NOT NULL, customer_ip_address varchar(40) NOT NULL, customer_user_agent varchar(200) NOT NULL, created_via varchar(200) NOT NULL, - date_completed datetime DEFAULT NULL, - date_paid datetime DEFAULT NULL, + date_completed varchar(20) DEFAULT NULL, + date_paid varchar(20) DEFAULT NULL, cart_hash varchar(32) NOT NULL, PRIMARY KEY (order_id), UNIQUE KEY `order_key` (`order_key`), diff --git a/includes/class-wc-custom-order-table.php b/includes/class-wc-custom-order-table.php index 08fb2f2..264f505 100644 --- a/includes/class-wc-custom-order-table.php +++ b/includes/class-wc-custom-order-table.php @@ -28,11 +28,11 @@ public function setup() { $this->table_name = $wpdb->prefix . 'woocommerce_orders'; - // Inject the plugin into order processing. + // Use the plugin's custom data stores for customers and orders. + add_filter( 'woocommerce_customer_data_store', array( $this, 'customer_data_store' ) ); add_filter( 'woocommerce_order_data_store', array( $this, 'order_data_store' ) ); - add_filter( 'posts_join', array( $this, 'wp_query_customer_query' ), 10, 2 ); - // Register the CLI command if we're running WP_CLI. + // If we're in a WP-CLI context, load the WP-CLI command. if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'wc-order-table', 'WC_Custom_Order_Table_CLI' ); } @@ -53,89 +53,20 @@ public function get_table_name() { } /** - * Retrieve the class name of the WooCommerce order data store. + * Retrieve the class name of the WooCommerce customer data store. * * @return string The data store class name. */ - public function order_data_store() { - return 'WC_Order_Data_Store_Custom_Table'; + public function customer_data_store() { + return 'WC_Customer_Data_Store_Custom_Table'; } /** - * Modify posts_join queries when the query includes wc_customer_query. - * - * @global $wpdb - * - * @param string $join The SQL JOIN statement. - * @param WP_Query $wp_query The current WP_Query object. - * - * @return string The [potentially] filtered JOIN statement. - */ - public function wp_query_customer_query( $join, $wp_query ) { - global $wpdb; - - // If there is no wc_customer_query then no need to process anything. - if ( ! isset( $wp_query->query_vars['wc_customer_query'] ) ) { - return $join; - } - - $customer_query = $this->generate_wc_customer_query( $wp_query->query_vars['wc_customer_query'] ); - $query_parts = array(); - - if ( ! empty( $customer_query['emails'] ) ) { - $emails = '\'' . implode( '\', \'', array_unique( $customer_query['emails'] ) ) . '\''; - $query_parts[] = "{$this->get_table_name()}.billing_email IN ( {$emails} )"; - } - - if ( ! empty( $customer_query['users'] ) ) { - $users = implode( ',', array_unique( $customer_query['users'] ) ); - $query_parts[] = "{$this->get_table_name()}.customer_id IN ( {$users} )"; - } - - if ( ! empty( $query_parts ) ) { - $query = '( ' . implode( ') OR (', $query_parts ) . ' )'; - $join .= "JOIN {$this->get_table_name()} ON - ( {$wpdb->posts}.ID = {$this->get_table_name()}.order_id ) - AND ( {$query} )"; - } - - return $join; - } - - /** - * Given a wc_customer_query argument, construct an array of customers grouped by either email - * address or user ID. - * - * @param array $values Query arguments from WP_Query->query_vars['wc_customer_query']. + * Retrieve the class name of the WooCommerce order data store. * - * @return array A complex array with two keys: "emails" and "users". + * @return string The data store class name. */ - public function generate_wc_customer_query( $values ) { - $customer_query = array( - 'emails' => array(), - 'users' => array(), - ); - - foreach ( $values as $value ) { - // If the value is an array, call this method recursively and merge the results. - if ( is_array( $value ) ) { - $query = $this->generate_wc_customer_query( $value ); - - if ( is_array( $query['emails'] ) ) { - $customer_query['emails'] = array_merge( $customer_query['emails'], $query['emails'] ); - } - - if ( is_array( $query['users'] ) ) { - $customer_query['users'] = array_merge( $customer_query['users'], $query['users'] ); - } - } elseif ( is_email( $value ) ) { - $customer_query['emails'][] = sanitize_email( $value ); - - } else { - $customer_query['users'][] = strval( absint( $value ) ); - } - } - - return $customer_query; + public function order_data_store() { + return 'WC_Order_Data_Store_Custom_Table'; } } diff --git a/includes/class-wc-customer-data-store-custom-table.php b/includes/class-wc-customer-data-store-custom-table.php new file mode 100644 index 0000000..e322934 --- /dev/null +++ b/includes/class-wc-customer-data-store-custom-table.php @@ -0,0 +1,231 @@ +get_table_name(); + $statuses = wc_get_order_statuses(); + $last_order = $wpdb->get_var( $wpdb->prepare( " + SELECT posts.ID FROM $wpdb->posts AS posts + LEFT JOIN " . esc_sql( $table ) . " AS meta on posts.ID = meta.order_id + WHERE meta.customer_id = %d + AND posts.post_type = 'shop_order' + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ') + ORDER BY posts.ID DESC LIMIT 1', + array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) + ) ); // WPCS: DB call OK. + + return $last_order ? wc_get_order( (int) $last_order ) : false; + } + + /** + * Return the number of orders this customer has. + * + * @global $wpdb + * + * @param WC_Customer $customer The WC_Customer object, passed by reference. + * + * @return int The number of orders for this customer. + */ + public function get_order_count( &$customer ) { + global $wpdb; + + $count = get_user_meta( $customer->get_id(), '_order_count', true ); + + if ( '' === $count ) { + $table = wc_custom_order_table()->get_table_name(); + $statuses = wc_get_order_statuses(); + $count = $wpdb->get_var( $wpdb->prepare( " + SELECT COUNT(*) FROM $wpdb->posts as posts + LEFT JOIN " . esc_sql( $table ) . " AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = %d + AND posts.post_type = 'shop_order' + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ')', + array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) + ) ); // WPCS: DB call OK. + update_user_meta( $customer->get_id(), '_order_count', $count ); + } + + return (int) $count; + } + + /** + * Return how much money this customer has spent. + * + * @global $wpdb + * + * @param WC_Customer $customer The WC_Customer object, passed by reference. + * + * @return float The total amount spent by the customer. + */ + public function get_total_spent( &$customer ) { + global $wpdb; + + $spent = get_user_meta( $customer->get_id(), '_money_spent', true ); + + /** + * Filter the total amount spent by the given customer across all orders. + * + * @param float $spent The total of all orders for this customer. + * @param WC_Customer $customer The customer being queried. + */ + $spent = apply_filters( 'woocommerce_customer_get_total_spent', $spent, $customer ); + + // If there's no saved value, attempt to calculate one. + if ( '' === $spent ) { + $table = wc_custom_order_table()->get_table_name(); + $statuses = array_map( 'self::prefix_wc_status', wc_get_is_paid_statuses() ); + $sql = $wpdb->prepare( " + SELECT SUM(meta.total) FROM $wpdb->posts as posts + LEFT JOIN " . esc_sql( $table ) . " AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = %d + AND posts.post_type = 'shop_order' + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ')', + array_merge( array( $customer->get_id() ), $statuses ) + ); + + /** + * Filter the MySQL query used to determine how much a customer has spent across all orders. + * + * @param string $sql The prepared MySQL statement. + * @param WC_Customer $customer The customer being queried. + */ + $sql = apply_filters( 'woocommerce_customer_get_total_spent_query', $sql, $customer ); + $spent = (float) $wpdb->get_var( $sql ); // WPCS: Unprepared SQL OK, DB call OK. + + update_user_meta( $customer->get_id(), '_money_spent', $spent ); + } + + return wc_format_decimal( $spent, 2 ); + } + + /** + * Short-circuit the wc_customer_bought_product() function. + * + * @see wc_customer_bought_product() + * + * @global $wpdb + * + * @param bool $purchased Whether or not the customer has purchased the product. + * @param string $customer_email Customer email to check. + * @param int $user_id User ID to check. + * @param int $product_id Product ID to check. + * + * @return bool Whether or not the customer has already purchased the given product ID. + */ + public static function pre_customer_bought_product( $purchased, $customer_email, $user_id, $product_id ) { + global $wpdb; + + $transient_name = 'wc_cbp_' . md5( $customer_email . $user_id . WC_Cache_Helper::get_transient_version( 'orders' ) ); + $result = get_transient( $transient_name ); + + if ( false === $result ) { + $customer_data = array( $user_id ); + + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( isset( $user->user_email ) ) { + $customer_data[] = $user->user_email; + } + } + + if ( is_email( $customer_email ) ) { + $customer_data[] = $customer_email; + } + + $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); + $statuses = array_map( 'self::prefix_wc_status', wc_get_is_paid_statuses() ); + + if ( 0 === count( $customer_data ) ) { + return false; + } + + $table = wc_custom_order_table()->get_table_name(); + $result = $wpdb->get_col( $wpdb->prepare( " + SELECT im.meta_value FROM {$wpdb->posts} AS p + INNER JOIN " . esc_sql( $table ) . " AS pm ON p.ID = pm.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id + WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ') + AND ( + pm.billing_email IN (' . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ') + OR pm.customer_id IN (' . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") + ) + AND im.meta_key IN ( '_product_id', '_variation_id' ) + AND im.meta_value != 0", + array_merge( $statuses, $customer_data, $customer_data ) + ) ); // WPCS: DB call OK. + $result = array_map( 'absint', $result ); + + set_transient( $transient_name, $result, DAY_IN_SECONDS * 30 ); + } + + return in_array( (int) $product_id, $result, true ); + } + + /** + * Reset customer_id on orders when a user is deleted. + * + * @param int $user_id The ID of the deleted user. + */ + public static function reset_order_customer_id_on_deleted_user( $user_id ) { + global $wpdb; + + $wpdb->update( + wc_custom_order_table()->get_table_name(), + array( 'customer_id' => 0 ), + array( 'customer_id' => $user_id ) + ); // WPCS: DB call OK. + } + + /** + * Helper function to prefix a status with 'wc-'. + * + * Statuses that already contain the prefix will be skipped. + * + * @param string $status The status to prefix. + * + * @return string The status with 'wc-' prefixed. + */ + public static function prefix_wc_status( $status ) { + if ( 'wc-' === substr( $status, 0, 3 ) ) { + return $status; + } + + return 'wc-' . $status; + } +} diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index e047b41..aa4c5dd 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -7,12 +7,12 @@ */ /** - * Extension of the Abstract_WC_Order_Data_Store_CPT class, designed to map data between - * WooCommerce and the custom database table. + * Extend the WC_Order_Data_Store_CPT class, overloading methods that require database access in + * order to use the new table. * - * Orders are still treated as posts within WordPress, but the data is stored in a separate table. + * Orders are still treated as posts within WordPress, but the meta is stored in a separate table. */ -class WC_Order_Data_Store_Custom_Table extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Data_Store_Interface { +class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { /** * Set to true when creating so we know to insert meta data. @@ -22,64 +22,68 @@ class WC_Order_Data_Store_Custom_Table extends Abstract_WC_Order_Data_Store_CPT protected $creating = false; /** - * Map table columns to related postmeta keys. - * - * @var array + * Hook into WooCommerce database queries related to orders. */ - protected $postmeta_mapping = array( - 'order_key' => '_order_key', - 'customer_id' => '_customer_user', - 'payment_method' => '_payment_method', - 'payment_method_title' => '_payment_method_title', - 'transaction_id' => '_transaction_id', - 'customer_ip_address' => '_customer_ip_address', - 'customer_user_agent' => '_customer_user_agent', - 'created_via' => '_created_via', - 'date_completed' => '_date_completed', - 'date_paid' => '_date_paid', - 'cart_hash' => '_cart_hash', - - 'billing_first_name' => '_billing_first_name', - 'billing_last_name' => '_billing_last_name', - 'billing_company' => '_billing_company', - 'billing_address_1' => '_billing_address_1', - 'billing_address_2' => '_billing_address_2', - 'billing_city' => '_billing_city', - 'billing_state' => '_billing_state', - 'billing_postcode' => '_billing_postcode', - 'billing_country' => '_billing_country', - 'billing_email' => '_billing_email', - 'billing_phone' => '_billing_phone', - - 'shipping_first_name' => '_shipping_first_name', - 'shipping_last_name' => '_shipping_last_name', - 'shipping_company' => '_shipping_company', - 'shipping_address_1' => '_shipping_address_1', - 'shipping_address_2' => '_shipping_address_2', - 'shipping_city' => '_shipping_city', - 'shipping_state' => '_shipping_state', - 'shipping_postcode' => '_shipping_postcode', - 'shipping_country' => '_shipping_country', - - 'discount_total' => '_cart_discount', - 'discount_tax' => '_cart_discount_tax', - 'shipping_total' => '_order_shipping', - 'shipping_tax' => '_order_shipping_tax', - 'cart_tax' => '_order_tax', - 'total' => '_order_total', - - 'version' => '_order_version', - 'currency' => '_order_currency', - 'prices_include_tax' => '_prices_include_tax', - ); + public function __construct() { + + // When creating a WooCommerce order data store request, filter the MySQL query. + add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', __CLASS__ . '::filter_database_queries', 10, 2 ); + } /** * Retrieve the database table column => post_meta mapping. * * @return array An array of database columns and their corresponding post_meta keys. */ - public function get_postmeta_mapping() { - return $this->postmeta_mapping; + public static function get_postmeta_mapping() { + return array( + 'order_key' => '_order_key', + 'customer_id' => '_customer_user', + 'payment_method' => '_payment_method', + 'payment_method_title' => '_payment_method_title', + 'transaction_id' => '_transaction_id', + 'customer_ip_address' => '_customer_ip_address', + 'customer_user_agent' => '_customer_user_agent', + 'created_via' => '_created_via', + 'date_completed' => '_date_completed', + 'date_paid' => '_date_paid', + 'cart_hash' => '_cart_hash', + + 'billing_index' => '_billing_address_index', + 'billing_first_name' => '_billing_first_name', + 'billing_last_name' => '_billing_last_name', + 'billing_company' => '_billing_company', + 'billing_address_1' => '_billing_address_1', + 'billing_address_2' => '_billing_address_2', + 'billing_city' => '_billing_city', + 'billing_state' => '_billing_state', + 'billing_postcode' => '_billing_postcode', + 'billing_country' => '_billing_country', + 'billing_email' => '_billing_email', + 'billing_phone' => '_billing_phone', + + 'shipping_index' => '_shipping_address_index', + 'shipping_first_name' => '_shipping_first_name', + 'shipping_last_name' => '_shipping_last_name', + 'shipping_company' => '_shipping_company', + 'shipping_address_1' => '_shipping_address_1', + 'shipping_address_2' => '_shipping_address_2', + 'shipping_city' => '_shipping_city', + 'shipping_state' => '_shipping_state', + 'shipping_postcode' => '_shipping_postcode', + 'shipping_country' => '_shipping_country', + + 'discount_total' => '_cart_discount', + 'discount_tax' => '_cart_discount_tax', + 'shipping_total' => '_order_shipping', + 'shipping_tax' => '_order_shipping_tax', + 'cart_tax' => '_order_tax', + 'total' => '_order_total', + + 'version' => '_order_version', + 'currency' => '_order_currency', + 'prices_include_tax' => '_prices_include_tax', + ); } /** @@ -88,14 +92,6 @@ public function get_postmeta_mapping() { * @param WC_Order $order The order object, passed by reference. */ public function create( &$order ) { - /** - * Filter the generated order ID. - * - * @param string $order_key The uniquely-generated ID for this order. - */ - $order_key = apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ); - - $order->set_order_key( 'wc_' . $order_key ); $this->creating = true; parent::create( $order ); @@ -116,13 +112,12 @@ public function delete( &$order, $args = array() ) { parent::delete( $order, $args ); - if ( $args['force_delete'] || 0 == $order->get_id() ) { + // Delete the database row if force_delete is true. + if ( isset( $args['force_delete'] ) && $args['force_delete'] ) { $wpdb->delete( "{$wpdb->prefix}woocommerce_orders", - array( - 'order_id' => $order_id, - ) - ); + array( 'order_id' => $order_id ) + ); // WPCS: DB call OK. } } @@ -135,8 +130,6 @@ public function delete( &$order, $args = array() ) { protected function read_order_data( &$order, $post_object ) { global $wpdb; - parent::read_order_data( $order, $post_object ); - $data = $this->get_order_data_from_table( $order ); if ( ! empty( $data ) ) { @@ -162,7 +155,16 @@ protected function read_order_data( &$order, $post_object ) { public function get_order_data_from_table( $order ) { global $wpdb; - return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_orders WHERE order_id = %d;", $order->get_id() ) ); + $table = wc_custom_order_table()->get_table_name(); + $data = $wpdb->get_row( $wpdb->prepare( + 'SELECT * FROM ' . esc_sql( $table ) . ' WHERE order_id = %d LIMIT 1', + $order->get_id() + ), ARRAY_A ); // WPCS: DB call OK. + + // Expand anything that might need assistance. + $data['prices_include_tax'] = wc_string_to_bool( $data['prices_include_tax'] ); + + return $data; } /** @@ -176,7 +178,10 @@ public function get_order_data_from_table( $order ) { protected function update_post_meta( &$order ) { global $wpdb; - $edit_data = array( + $table = wc_custom_order_table()->get_table_name(); + $changes = array(); + $order_data = array( + 'order_id' => $order->get_id( 'edit' ), 'order_key' => $order->get_order_key( 'edit' ), 'customer_id' => $order->get_customer_id( 'edit' ), 'payment_method' => $order->get_payment_method( 'edit' ), @@ -189,6 +194,7 @@ protected function update_post_meta( &$order ) { 'date_paid' => $order->get_date_paid( 'edit' ), 'cart_hash' => $order->get_cart_hash( 'edit' ), + 'billing_index' => implode( ' ', $order->get_address( 'billing' ) ), 'billing_first_name' => $order->get_billing_first_name( 'edit' ), 'billing_last_name' => $order->get_billing_last_name( 'edit' ), 'billing_company' => $order->get_billing_company( 'edit' ), @@ -202,6 +208,7 @@ protected function update_post_meta( &$order ) { 'billing_email' => $order->get_billing_email( 'edit' ), 'billing_phone' => $order->get_billing_phone( 'edit' ), + 'shipping_index' => implode( ' ', $order->get_address( 'shipping' ) ), 'shipping_first_name' => $order->get_shipping_first_name( 'edit' ), 'shipping_last_name' => $order->get_shipping_last_name( 'edit' ), 'shipping_company' => $order->get_shipping_company( 'edit' ), @@ -216,44 +223,54 @@ protected function update_post_meta( &$order ) { 'discount_tax' => $order->get_discount_tax( 'edit' ), 'shipping_total' => $order->get_shipping_total( 'edit' ), 'shipping_tax' => $order->get_shipping_tax( 'edit' ), - 'cart_tax' => $order->get_total_tax( 'edit' ), + 'cart_tax' => $order->get_cart_tax( 'edit' ), 'total' => $order->get_total( 'edit' ), 'version' => $order->get_version( 'edit' ), 'currency' => $order->get_currency( 'edit' ), - 'prices_include_tax' => $order->get_prices_include_tax( 'edit' ), + 'prices_include_tax' => wc_bool_to_string( $order->get_prices_include_tax( 'edit' ) ), ); - $changes = array(); + // Convert dates to timestamps, if they exist. + foreach ( array( 'date_completed', 'date_paid' ) as $date ) { + if ( $order_data[ $date ] instanceof WC_DateTime ) { + $order_data[ $date ] = $order_data[ $date ]->getTimestamp(); + } + } + // Insert or update the database record. if ( $this->creating ) { - $wpdb->insert( - "{$wpdb->prefix}woocommerce_orders", - array_merge( array( - 'order_id' => $order->get_id(), - ), $edit_data ) - ); + $wpdb->insert( $table, $order_data ); // WPCS: DB call OK. - // We are no longer creating the order, it is created. $this->creating = false; + } else { - $changes = array_intersect_key( $edit_data, $order->get_changes() ); + $changes = array_intersect_key( $order_data, $order->get_changes() ); + + /* + * WC_Order::get_changes() will mark all address fields as changed if one has changed. + * + * If any of these fields are present, be sure we update the index column. + */ + if ( isset( $changes['billing_first_name'] ) ) { + $changes['billing_index'] = $order_data['billing_index']; + } + + if ( isset( $changes['shipping_first_name'] ) ) { + $changes['shipping_index'] = $order_data['shipping_index']; + } if ( ! empty( $changes ) ) { - $wpdb->update( - "{$wpdb->prefix}woocommerce_orders", - $changes, - array( - 'order_id' => $order->get_id(), - ) - ); + $wpdb->update( $table, $changes, array( 'order_id' => $order->get_id() ) ); // WPCS: DB call OK. } } $updated_props = array_keys( (array) $changes ); // If customer changed, update any downloadable permissions. - if ( in_array( 'customer_user', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { + $customer_props = array( 'customer_user', 'billing_email' ); + + if ( array_intersect( $customer_props, $updated_props ) ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() ); } @@ -261,88 +278,6 @@ protected function update_post_meta( &$order ) { do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); } - /** - * Excerpt for post. - * - * @param WC_Order $order The order object. - * - * @return string The post excerpt. - */ - protected function get_post_excerpt( $order ) { - return $order->get_customer_note(); - } - - /** - * Get amount already refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return string The refund amount. - */ - public function get_total_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( postmeta.meta_value ) - FROM $wpdb->postmeta AS postmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - WHERE postmeta.meta_key = '_refund_amount' - AND postmeta.post_id = posts.ID - ", $order->get_id() ) ); - - return $total; - } - - /** - * Get the total tax refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return float The total refunded tax. - */ - public function get_total_tax_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount') - ", $order->get_id() ) ); - - return abs( $total ); - } - - /** - * Get the total shipping refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return float The total refunded shipping. - */ - public function get_total_shipping_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('cost') - ", $order->get_id() ) ); - - return abs( $total ); - } - /** * Finds an order ID based on its order key. * @@ -356,192 +291,7 @@ public function get_order_id_by_order_key( $order_key ) { return $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_orders WHERE order_key = %s", $order_key - ) ); - } - - /** - * Return count of orders with a specific status. - * - * @global $wpdb - * - * @param string $status The post_status to filter orders by. - * - * @return int The number of orders with that status. - */ - public function get_order_count( $status ) { - global $wpdb; - - return absint( $wpdb->get_var( $wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", - $status - ) ) ); - } - - /** - * Get all orders matching the passed in args. - * - * @see wc_get_orders() - * - * @param array $args { - * Query arguments. All arguments are optional. - * - * @var string $type The post type. Default is 'shop_order'. - * @var - * } - * @return object|array array of orders - */ - public function get_orders( $args = array() ) { - /** - * Generate WP_Query args. This logic will change if orders are moved to - * custom tables in the future. - */ - $wp_query_args = array( - 'post_type' => $args['type'] ? $args['type'] : 'shop_order', - 'post_status' => $args['status'], - 'posts_per_page' => $args['limit'], - 'meta_query' => array(), - 'fields' => 'ids', - 'orderby' => $args['orderby'], - 'order' => $args['order'], - ); - $wc_customer_query = array(); - - if ( ! empty( $args['customer'] ) ) { - $values = is_array( $args['customer'] ) ? $args['customer'] : array( $args['customer'] ); - $wc_customer_query = array_merge( $wc_customer_query, $values ); - } - - if ( ! empty( $args['email'] ) ) { - $values = is_array( $args['email'] ) ? $args['email'] : array( $args['email'] ); - $wc_customer_query = array_merge( $wc_customer_query, $values ); - } - - if ( ! empty( $wc_customer_query ) ) { - $wp_query_args['wc_customer_query'] = $wc_customer_query; - } - - /** - * Standard Args - */ - if ( ! is_null( $args['parent'] ) ) { - $wp_query_args['post_parent'] = absint( $args['parent'] ); - } - - if ( ! is_null( $args['offset'] ) ) { - $wp_query_args['offset'] = absint( $args['offset'] ); - } else { - $wp_query_args['paged'] = absint( $args['page'] ); - } - - if ( ! empty( $args['exclude'] ) ) { - $wp_query_args['post__not_in'] = array_map( 'absint', $args['exclude'] ); - } - - if ( ! $args['paginate'] ) { - $wp_query_args['no_found_rows'] = true; - } - - if ( ! empty( $args['date_before'] ) ) { - $wp_query_args['date_query']['before'] = $args['date_before']; - } - - if ( ! empty( $args['date_after'] ) ) { - $wp_query_args['date_query']['after'] = $args['date_after']; - } - - /** - * Filter WP_Query arguments when retrieving orders from the database. - * - * @param array $wp_query_args The current WP_Query arguments. - * @param array $args Raw arguments passed to WC_Order_Data_Store_Custom_Table::get_orders(). - * @param WC_Order_Data_Store_Custom_Table $instance The current instance of the WC_Order_Data_Store_Custom_Table class. - */ - $wp_query_args = apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $args, $this ); - $orders = new WP_Query( $wp_query_args ); - - if ( 'objects' === $args['return'] ) { - $return = array_map( 'wc_get_order', $orders->posts ); - } else { - $return = $orders->posts; - } - - if ( $args['paginate'] ) { - return (object) array( - 'orders' => $return, - 'total' => $orders->found_posts, - 'max_num_pages' => $orders->max_num_pages, - ); - } else { - return $return; - } - } - - /** - * Generate meta query for wc_get_orders. - * - * @param array $values Values to populate the meta query. - * @param string $relation Optional The query relationship, either "and" or "or". Default is "or". - * - * @return array An array suitable for passing to WP_Query's meta_query argument. - */ - private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { - $meta_query = array( - 'relation' => strtoupper( $relation ), - 'customer_emails' => array( - 'key' => '_billing_email', - 'value' => array(), - 'compare' => 'IN', - ), - 'customer_ids' => array( - 'key' => '_customer_user', - 'value' => array(), - 'compare' => 'IN', - ), - ); - - foreach ( $values as $value ) { - if ( is_array( $value ) ) { - $meta_query[] = $this->get_orders_generate_customer_meta_query( $value, 'and' ); - } elseif ( is_email( $value ) ) { - $meta_query['customer_emails']['value'][] = sanitize_email( $value ); - } else { - $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); - } - } - - if ( empty( $meta_query['customer_emails']['value'] ) ) { - unset( $meta_query['customer_emails'] ); - unset( $meta_query['relation'] ); - } - - if ( empty( $meta_query['customer_ids']['value'] ) ) { - unset( $meta_query['customer_ids'] ); - unset( $meta_query['relation'] ); - } - - return $meta_query; - } - - /** - * Get unpaid orders after a certain date. - * - * @param int $date The Unix timestamp used for date filtering. - * - * @return array An array of unpaid orders. - */ - public function get_unpaid_orders( $date ) { - global $wpdb; - - $order_types = wc_get_order_types(); - - return $wpdb->get_col( $wpdb->prepare( - "SELECT ID - FROM $wpdb->posts - WHERE post_type IN (" . implode( ',', array_fill( 0, count( $order_types ), '%s' ) ) . ") - AND post_status = 'wc-pending' - AND post_modified < %s", - array_merge( $order_types, array( date( 'Y-m-d H:i:s', (int) $date ) ) ) - ) ); + ) ); // WPCS: DB call OK. } /** @@ -554,162 +304,78 @@ public function get_unpaid_orders( $date ) { public function search_orders( $term ) { global $wpdb; - $order_ids = array(); - - // Treat a numeric search term as an order ID. - if ( is_numeric( $term ) ) { - $order_ids[] = absint( $term ); - } - - // Search given post meta columns for the query. - $postmeta_search = array(); - /** * Searches on meta data can be slow - this lets you choose what fields to search. + * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. + * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). * - * WooCommerce 2.7.0 added _billing_address and _shipping_address meta which contains all - * address data to make this faster. However, this won't work on older orders unless they - * are updated, so search a few others (expand this using the filter if needed). + * @var array */ - $meta_search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_shop_order_search_fields', array() ) ); - - // If we were given meta fields to search, make it happen. - if ( ! empty( $meta_search_fields ) ) { - $postmeta_search = $wpdb->get_col( $wpdb->prepare( " - SELECT DISTINCT post_id - FROM {$wpdb->postmeta} - WHERE meta_key IN (" . implode( ',', array_fill( 0, count( $meta_search_fields ), '%s' ) ) . ') - AND meta_value LIKE %s - ', - array_merge( $meta_search_fields, array( '%' . $wpdb->esc_like( $term ) . '%' ) ) - ) ); - } - - return array_unique( array_merge( - $order_ids, - $postmeta_search, - $wpdb->get_col( - $wpdb->prepare( " - SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items as order_items - WHERE order_item_name LIKE %s - ", - '%' . $wpdb->esc_like( $term ) . '%' + $search_fields = array_map( + 'wc_clean', apply_filters( + 'woocommerce_shop_order_search_fields', array( + '_billing_address_index', + '_shipping_address_index', + '_billing_last_name', + '_billing_email', ) ) - ) ); - } - - /** - * Gets information about whether permissions were generated yet. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_download_permissions_granted( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); - } - - /** - * Stores information about whether permissions were generated yet. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the permissions have been generated. - */ - public function set_download_permissions_granted( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether sales were recorded. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_recorded_sales( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); - } - - /** - * Stores information about whether sales were recorded. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the sales have been recorded. - */ - public function set_recorded_sales( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); - } + ); + $term = wc_clean( $term ); + $order_ids = array(); - /** - * Gets information about whether coupon counts were updated. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_recorded_coupon_usage_counts( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); + // Treat a numeric search term as an order ID. + if ( is_numeric( $term ) ) { + $order_ids[] = absint( $term ); + } - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); - } + // Search for order meta fields. + if ( ! empty( $search_fields ) ) { + $mapping = self::get_postmeta_mapping(); + $in_table = array_intersect( $search_fields, $mapping ); + $meta_keys = array_diff( $search_fields, $in_table ); + $table = wc_custom_order_table()->get_table_name(); - /** - * Stores information about whether coupon counts were updated. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the coupon counts have been updated. - */ - public function set_recorded_coupon_usage_counts( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); + // Find results based on search fields that map to table columns. + if ( ! empty( $in_table ) ) { + $columns = array_keys( array_intersect( $mapping, $in_table ) ); + $where = array(); - update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); - } + foreach ( $columns as $column ) { + $where[] = "{$column} LIKE %s"; + } - /** - * Gets information about whether stock was reduced. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_stock_reduced( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( + 'SELECT DISTINCT order_id FROM ' . esc_sql( $table ) . ' WHERE ' . implode( ' OR ', $where ), + array_fill( 0, count( $where ), '%' . $wpdb->esc_like( $term ) . '%' ) + ) ) ); // WPCS: DB call OK, Unprepared SQL ok, PreparedSQLPlaceholders replacement count ok. + } - return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); - } + // For anything else, fall back to postmeta. + if ( ! empty( $meta_keys ) ) { + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " + SELECT DISTINCT post_id FROM {$wpdb->postmeta} + WHERE meta_value LIKE %s + AND meta_key IN (" . implode( ',', array_fill( 0, count( $meta_keys ), '%s' ) ) . ')', + array_merge( + array( '%' . $wpdb->esc_like( $term ) . '%' ), + $meta_keys + ) + ) ) ); // WPCS: DB call OK. + } + } - /** - * Stores information about whether stock was reduced. - * - * @param WC_Order|int $order The order object or ID. - * - * @param bool $set Whether or not stock has been reduced. - */ - public function set_stock_reduced( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); + // Search item names. + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " + SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_name LIKE %s", + '%' . $wpdb->esc_like( $term ) . '%' + ) ) ); // WPCS: DB call OK. - update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); - } + // Reduce the array of order IDs to unique values. + $order_ids = array_unique( $order_ids ); - /** - * Get the order type based on Order ID. - * - * @param int $order_id The order ID. - * - * @return string The order post type. - */ - public function get_order_type( $order_id ) { - return get_post_type( $order_id ); + return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); } /** @@ -731,7 +397,7 @@ public function populate_from_meta( &$order, $save = true, $delete = false ) { $this->creating = true; } - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { $meta = get_post_meta( $order->get_id(), $meta_key, true ); if ( empty( $table_data->$column ) && ! empty( $meta ) ) { @@ -751,7 +417,7 @@ public function populate_from_meta( &$order, $save = true, $delete = false ) { } if ( true === $delete ) { - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { delete_post_meta( $order->get_id(), $meta_key ); } } @@ -773,7 +439,7 @@ public function backfill_postmeta( &$order ) { return; } - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { if ( isset( $data->$column ) ) { update_post_meta( $order->get_id(), $meta_key, $data->$column ); } @@ -781,35 +447,127 @@ public function backfill_postmeta( &$order ) { } /** - * Query for Orders matching specific criteria. + * Determine if any filters are required on the MySQL query and, if so, apply them. * - * @param array $query_vars Query arguments from a WC_Order_Query. + * @param array $query_args The arguments to be passed to WP_Query. + * @param array $query_vars The raw query vars passed to build the query. * - * @return array|object + * @return array The potentially-filtered $query_args array. */ - public function query( $query_vars ) { - $args = $this->get_wp_query_args( $query_vars ); - - if ( ! empty( $args['errors'] ) ) { - $query = (object) array( - 'posts' => array(), - 'found_posts' => 0, - 'max_num_pages' => 0, - ); - } else { - $query = new WP_Query( $args ); + public static function filter_database_queries( $query_args, $query_vars ) { + $query_args['wc_order_meta_query'] = array(); + $query_args['_wc_has_meta_columns'] = false; + + // Iterate over the meta_query to find special cases. + if ( isset( $query_args['meta_query'] ) ) { + foreach ( $query_args['meta_query'] as $index => $meta_query ) { + + // Flatten complex meta queries. + if ( is_array( $meta_query ) && 1 === count( $meta_query ) && is_array( current( $meta_query ) ) ) { + $meta_query = current( $meta_query ); + } + + if ( isset( $meta_query['customer_emails'] ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query['customer_emails'], array( + 'key' => 'billing_email', + '_old_key' => $meta_query['customer_emails']['key'], + ) ); + } + + if ( isset( $meta_query['customer_ids'] ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query['customer_ids'], array( + 'key' => 'customer_id', + '_old_key' => $meta_query['customer_ids']['key'], + ) ); + } + + if ( isset( $meta_query['key'] ) ) { + $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ); + + if ( $column ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( + 'key' => $column, + '_old_key' => $meta_query['key'], + ) ); + } + } else { + // Let this meta query pass through unaltered. + $query_args['_wc_has_meta_columns'] = true; + } + } } - $orders = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_order', $query->posts ) ); + // Add filters to address specific portions of the query. + add_filter( 'posts_join', __CLASS__ . '::posts_join', 10, 2 ); + add_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); - if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { - return (object) array( - 'orders' => $orders, - 'total' => $query->found_posts, - 'max_num_pages' => $query->max_num_pages, - ); + return $query_args; + } + + /** + * Filter the JOIN statement generated by WP_Query. + * + * @global $wpdb + * + * @param string $join The MySQL JOIN statement. + * @param WP_Query $wp_query The WP_Query object, passed by reference. + * + * @return string The filtered JOIN statement. + */ + public static function posts_join( $join, $wp_query ) { + global $wpdb; + + /* + * Remove the now-unnecessary INNER JOIN with the post_meta table unless there's some post + * meta that doesn't have a column in the custom table. + * + * @see WP_Meta_Query::get_sql_for_clause() + */ + if ( ! $wp_query->get( '_wc_has_meta_columns', false ) ) { + // Match the post_meta table INNER JOIN, with or without an alias. + $regex = "/\sINNER\sJOIN\s{$wpdb->postmeta}\s+(AS\s[^\s]+)?\s*ON\s\([^\)]+\)/i"; + + $join = preg_replace( $regex, '', $join ); } - return $orders; + $table = wc_custom_order_table()->get_table_name(); + $join .= " LEFT JOIN {$table} ON ( {$wpdb->posts}.ID = {$table}.order_id ) "; + + // Don't necessarily apply this to subsequent posts_join filter callbacks. + remove_filter( 'posts_join', __CLASS__ . '::posts_join', 10, 2 ); + + return $join; + } + + /** + * Filter the "WHERE" portion of the MySQL query to look at the custom orders table instead of + * post meta. + * + * @global $wpdb + * + * @param string $where The MySQL WHERE statement. + * @param WP_Query $wp_query The WP_Query object, passed by reference. + * + * @return string The [potentially-] filtered WHERE statement. + */ + public static function meta_query_where( $where, $wp_query ) { + global $wpdb; + + $meta_query = $wp_query->get( 'wc_order_meta_query' ); + $table = wc_custom_order_table()->get_table_name(); + + if ( empty( $meta_query ) ) { + return $where; + } + + foreach ( $meta_query as $query ) { + $regex = $wpdb->prepare( '/\(\s?(\w+\.)?meta_key = %s AND (\w+\.)?meta_value /i', $query['_old_key'] ); + $where = preg_replace( $regex, "( {$table}.{$query['key']} ", $where ); + } + + // Ensure this doesn't affect all subsequent queries. + remove_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); + + return $where; } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 56337d6..242bd78 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,8 +2,11 @@ Generally-applicable sniffs for WordPress plugins - - + + + + + @@ -12,7 +15,6 @@ - */node_modules/* - */vendor/* + */tests/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5d2d5d1..58ff400 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,10 +8,16 @@ convertWarningsToExceptions="true" > - + + ./tests/ tests/test-sample.php + + + + ./vendor/woocommerce/woocommerce/tests/unit-tests + diff --git a/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh similarity index 100% rename from bin/install-wp-tests.sh rename to tests/bin/install-wp-tests.sh diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8951aba..c09a5ed 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,91 +1,39 @@ get_error_code() ) { - include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + WC_Custom_Order_Table_Install::activate(); - echo PHP_EOL . "WooCommerce is not currently installed in the test environment, attempting to install..."; - - // Retrieve information about WooCommerce. - $plugin_data = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/woocommerce.json' ); - - if ( ! is_wp_error( $plugin_data ) ) { - $plugin_data = json_decode( wp_remote_retrieve_body( $plugin_data ) ); - $plugin_url = $plugin_data->download_link; - } else { - $plugin_url = false; - } - - // Download the plugin from the WordPress.org repository. - $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); - $installed = $upgrader->install( $plugin_url ); - - if ( true === $installed ) { - echo "\033[0;32mOK\033[0;m" . PHP_EOL . PHP_EOL; - } else { - echo "\033[0;31mFAIL\033[0;m" . PHP_EOL; - - if ( is_wp_error( $installed ) ) { - printf( 'Unable to install WooCommerce: %s.', $installed->get_error_message() ); - } - - printf( - 'Please download and install WooCommerce into %s' . PHP_EOL, - trailingslashit(dirname( dirname( $_tests_dir ) ) ) - ); - - exit( 1 ); - } - - // Try once again to activate. - $activated = activate_plugin( 'woocommerce/woocommerce.php' ); + add_filter( 'woocommerce_email_actions', '__return_empty_array' ); } +tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); -// Nothing more we can do, unfortunately. -if ( is_wp_error( $activated ) ) { - echo PHP_EOL . 'WooCommerce could not automatically be activated in the test environment:'; - echo PHP_EOL . $activated->get_error_message(); - echo PHP_EOL . PHP_EOL . "\033[0;31mUnable to proceed with tests, aborting.\033[0m"; - echo PHP_EOL; - exit( 1 ); -} +// Finally, Start up the WP testing environment. +require_once $_bootstrap; +require_once __DIR__ . '/testcase.php'; diff --git a/tests/test-bootstrap.php b/tests/test-bootstrap.php index 700ba75..f7ec170 100644 --- a/tests/test-bootstrap.php +++ b/tests/test-bootstrap.php @@ -8,25 +8,11 @@ class BootstrapTest extends TestCase { - /** - * Tear down the plugin after each test run. - * - * @before - */ - public function tear_down_plugin() { - global $wc_custom_order_table; - - // Destroy the global $wc_custom_order_table instance. - unset( $wc_custom_order_table ); - } - public function test_plugin_only_loads_after_woocommerce() { global $wc_custom_order_table; - $this->assertNull( - $wc_custom_order_table, - 'Before bootstrapping, $wc_custom_order_table should be empty.' - ); + // Test bootstrapping may have initialized it for us. + $wc_custom_order_table = null; do_action( 'woocommerce_init' ); diff --git a/tests/test-data-store.php b/tests/test-data-store.php index c313f7e..791efad 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -9,85 +9,73 @@ class DataStoreTest extends TestCase { /** - * Fire the necessary actions to bootstrap WordPress. - * - * @before + * @requires PHP 5.4 In order to support inline closures for hook callbacks. */ - public function init() { - do_action( 'init' ); - } - - /** - * Remove any closures that have been assigned to filters. - * - * @after - */ - public function remove_filter_callbacks() { - remove_all_filters( 'woocommerce_shop_order_search_fields' ); - } - public function test_create() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create_and_get(); - $order_key = 'my_custom_order_key'; - - add_filter( 'woocommerce_generate_order_key', function () use ( $order_key ) { - return $order_key; + $instance = new WC_Order_Data_Store_Custom_Table(); + $property = new ReflectionProperty( $instance, 'creating' ); + $property->setAccessible( true ); + $order = new WC_Order( wp_insert_post( array( + 'post_type' => 'product', + ) ) ); + + add_action( 'wp_insert_post', function () use ( $property, $instance ) { + $this->assertTrue( + $property->getValue( $instance ), + 'As an order is being created, WC_Order_Data_Store_Custom_Table::$creating should be true' + ); } ); $instance->create( $order ); - $this->assertEquals( 'wc_' . $order_key, $order->get_order_key() ); + $this->assertEquals( 1, did_action( 'wp_insert_post' ), 'Expected the "wp_insert_post" action to have been fired.' ); } - public function test_get_order_count() { + public function test_delete() { $instance = new WC_Order_Data_Store_Custom_Table(); - $orders = $this->factory()->order->create_many( 5, array( - 'post_status' => 'wc-pending', - ) ); + $order = WC_Helper_Order::create_order(); - $this->assertEquals( - count( $orders ), - $instance->get_order_count( 'wc-pending' ) + $instance->delete( $order, array( 'force_delete' => false ) ); + + $this->assertNotNull( + $this->get_order_row( $order->get_id() ), + 'Unless force_delete is true, the table row should not be removed.' ); } - public function test_get_order_count_filters_by_status() { + public function test_delete_can_force_delete() { $instance = new WC_Order_Data_Store_Custom_Table(); - $this->factory()->order->create( array( - 'post_status' => 'not_a_pending_status', - ) ); + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); - $this->assertEquals( - 0, - $instance->get_order_count( 'wc-pending' ), - 'The get_order_count() method should only count records matching $status.' - ); + $instance->delete( $order, array( 'force_delete' => true ) ); + + $this->assertNull( $this->get_order_row( $order_id ), 'When force deleting, the table row should be removed.' ); } - public function test_get_unpaid_orders() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_status' => 'wc-pending', - ) ); - $pending = $instance->get_unpaid_orders( time() + DAY_IN_SECONDS ); + public function test_update_post_meta_for_new_order() { + $order = new WC_Order( wp_insert_post( array( + 'post_type' => 'product', + ) ) ); + $order->set_currency( 'USD' ); + $order->set_prices_include_tax( false ); + $order->set_customer_ip_address( '127.0.0.1' ); + $order->set_customer_user_agent( 'PHPUnit' ); - $this->assertCount( 1, $pending, 'There should be only one unpaid order.' ); - $this->assertEquals( - $order, - array_shift( $pending ), - 'The ID of the one unpaid order should be that of $order.' - ); + $this->invoke_update_post_meta( $order, true ); + + $row = $this->get_order_row( $order->get_id() ); + + $this->assertEquals( 'USD', $row['currency'] ); + $this->assertEquals( '127.0.0.1', $row['customer_ip_address'] ); + $this->assertEquals( 'PHPUnit', $row['customer_user_agent'] ); } - public function test_get_unpaid_orders_uses_date_filtering() { + public function test_get_order_id_by_order_key() { + $order = WC_Helper_Order::create_order(); $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_status' => 'wc-pending', - ) ); - $pending = $instance->get_unpaid_orders( time() - HOUR_IN_SECONDS ); - $this->assertEmpty( $pending, 'No unpaid orders should match the time window.' ); + $this->assertEquals( $order->get_id(), $instance->get_order_id_by_order_key( $order->get_order_key() ) ); } public function test_search_orders_can_search_by_order_id() { @@ -102,20 +90,29 @@ public function test_search_orders_can_search_by_order_id() { public function test_search_orders_can_check_post_meta() { $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create(); + $order = WC_Helper_Order::create_order(); $term = uniqid( 'search term ' ); - add_post_meta( $order, 'some_custom_meta_key', $term ); + add_post_meta( $order->get_id(), 'some_custom_meta_key', $term ); - add_filter( 'woocommerce_shop_order_search_fields', function () { - return array( 'some_custom_meta_key' ); - } ); + add_filter( 'woocommerce_shop_order_search_fields', __CLASS__ . '::return_array_for_test_search_orders_can_check_post_meta' ); $this->assertEquals( - array( $order ), + array( $order->get_id() ), $instance->search_orders( $term ), 'If post meta keys are specified, they should also be searched.' ); + + remove_filter( 'woocommerce_shop_order_search_fields', __CLASS__ . '::return_array_for_test_search_orders_can_check_post_meta' ); + } + + /** + * Filter callback for test_search_orders_can_check_post_meta(). + * + * Can be dropped once PHP 5.3 isn't a requirement, as closures are far nicer. + */ + public static function return_array_for_test_search_orders_can_check_post_meta() { + return array( 'some_custom_meta_key' ); } /** @@ -123,10 +120,10 @@ public function test_search_orders_can_check_post_meta() { */ public function test_search_orders_only_checks_post_meta_if_specified() { $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create(); + $order = WC_Helper_Order::create_order(); $term = uniqid( 'search term ' ); - add_post_meta( $order, 'some_custom_meta_key', $term ); + add_post_meta( $order->get_id(), 'some_custom_meta_key', $term ); $this->assertEmpty( $instance->search_orders( $term ), @@ -136,8 +133,8 @@ public function test_search_orders_only_checks_post_meta_if_specified() { public function test_search_orders_checks_table_for_product_item_matches() { $instance = new WC_Order_Data_Store_Custom_Table(); - $product = $this->factory()->product->create_and_get(); - $order = $this->factory()->order->create_and_get(); + $product = WC_Helper_Product::create_simple_product(); + $order = WC_Helper_Order::create_order(); $order->add_product( $product ); $order->save(); @@ -150,10 +147,10 @@ public function test_search_orders_checks_table_for_product_item_matches() { public function test_search_orders_checks_table_for_product_item_matches_with_like_comparison() { $instance = new WC_Order_Data_Store_Custom_Table(); - $product = $this->factory()->product->create_and_get( array( - 'post_title' => 'foo bar baz', - ) ); - $order = $this->factory()->order->create_and_get(); + $product = WC_Helper_Product::create_simple_product(); + $product->set_name( 'Foo Bar Baz' ); + $product->save(); + $order = WC_Helper_Order::create_order(); $order->add_product( $product ); $order->save(); @@ -165,30 +162,21 @@ public function test_search_orders_checks_table_for_product_item_matches_with_li } /** - * @dataProvider order_type_provider() + * Shortcut for setting up reflection methods + properties for update_post_meta(). + * + * @param WC_Order $order The order object, passed by reference. + * @param bool $creating Optional. The value 'creating' property in the new instance. + * Default is false. */ - public function test_get_order_type( $order_type ) { + protected function invoke_update_post_meta( &$order, $creating = false ) { $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_type' => $order_type, - ) ); - - $this->assertEquals( - $order_type, - $instance->get_order_type( $order ) - ); - } - - /** - * Provide a list of all available order types. - */ - public function order_type_provider() { - $types = array(); - foreach ( wc_get_order_types() as $type ) { - $types[ $type ] = array( $type ); - } + $property = new ReflectionProperty( $instance, 'creating' ); + $property->setAccessible( true ); + $property->setValue( $instance, (bool) $creating ); - return $types; + $method = new ReflectionMethod( $instance, 'update_post_meta' ); + $method->setAccessible( true ); + $method->invokeArgs( $instance, array( &$order ) ); } } diff --git a/tests/test-tools/class-wp-unittest-factory-for-customers.php b/tests/test-tools/class-wp-unittest-factory-for-customers.php deleted file mode 100644 index 3cb8df7..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-customers.php +++ /dev/null @@ -1,27 +0,0 @@ -default_generation_definitions = array( - 'user_login' => new WP_UnitTest_Generator_Sequence( 'Customer %s' ), - 'user_pass' => 'password', - 'user_email' => new WP_UnitTest_Generator_Sequence( 'customer_%s@example.org' ), - ); - } - - function get_object_by_id( $user_id ) { - return new WC_Customer( $user_id ); - } -} diff --git a/tests/test-tools/class-wp-unittest-factory-for-order.php b/tests/test-tools/class-wp-unittest-factory-for-order.php deleted file mode 100644 index 4758661..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-order.php +++ /dev/null @@ -1,28 +0,0 @@ -default_generation_definitions = array( - 'post_status' => 'wc-pending', - 'post_title' => new WP_UnitTest_Generator_Sequence( 'Order %s' ), - 'post_password' => uniqid( 'wc_' ), - 'post_type' => 'shop_order', - ); - } - - function get_object_by_id( $order_id ) { - return wc_get_order( $order_id ); - } -} diff --git a/tests/test-tools/class-wp-unittest-factory-for-product.php b/tests/test-tools/class-wp-unittest-factory-for-product.php deleted file mode 100644 index 9edaccc..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-product.php +++ /dev/null @@ -1,29 +0,0 @@ -default_generation_definitions = array( - 'post_status' => 'publish', - 'post_title' => new WP_UnitTest_Generator_Sequence( 'Product name %s' ), - 'post_content' => new WP_UnitTest_Generator_Sequence( 'Product description %s' ), - 'post_excerpt' => new WP_UnitTest_Generator_Sequence( 'Product short description %s' ), - 'post_type' => 'product', - ); - } - - function get_object_by_id( $product_id ) { - return wc_get_product( $product_id ); - } -} diff --git a/tests/testcase.php b/tests/testcase.php index 1b1eb4c..575305e 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -6,29 +6,37 @@ * @author Liquid Web */ -class TestCase extends WP_UnitTestCase { +class TestCase extends WC_Unit_Test_Case { /** - * Retrieve the core test suite's factory object, but add extra factories. + * Delete all data from the orders table after each test. * - * @return WP_UnitTest_Factory + * @after + * + * @global $wpdb */ - protected static function factory() { - static $factory = null; - - if ( ! $factory ) { - $instance = new WP_UnitTest_Factory(); + function truncate_table() { + global $wpdb; - // Add additional factories. - $instance->customer = new WP_UnitTest_Factory_For_Customer( $instance ); - $instance->order = new WP_UnitTest_Factory_For_Order( $instance ); - $instance->product = new WP_UnitTest_Factory_For_Product( $instance ); + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_orders" ); + } - // Save the instance in the static $factory variable. - $factory = $instance; - } + /** + * Retrieve a single row from the Orders table. + * + * @global $wpdb + * + * @param int $order_id The order ID to retrieve. + * + * @return array|null The contents of the database row or null if the given row doesn't exist. + */ + protected function get_order_row( $order_id ) { + global $wpdb; - return $factory; + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_orders WHERE order_id = %d", + $order_id + ), ARRAY_A ); } /** diff --git a/wc-custom-order-table.php b/wc-custom-order-table.php index c16be92..bffa17b 100644 --- a/wc-custom-order-table.php +++ b/wc-custom-order-table.php @@ -22,7 +22,7 @@ /* Load includes via a PHP 5.2-compatible autoloader. */ if ( file_exists( WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php' ) ) { - require( WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php' ); + require WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php'; } /** @@ -43,7 +43,7 @@ function wc_custom_order_table() { global $wc_custom_order_table; if ( ! $wc_custom_order_table instanceof WC_Custom_Order_Table ) { - $wc_custom_order_table = new WC_Custom_Order_Table; + $wc_custom_order_table = new WC_Custom_Order_Table(); $wc_custom_order_table->setup(); }