From 1797c76a148843286050ec0d6639daf7feeca667 Mon Sep 17 00:00:00 2001 From: Justin Shreve Date: Tue, 8 Nov 2016 01:39:47 -0800 Subject: [PATCH] Implement WC_Data_Store and related code & tests. --- includes/abstracts/abstract-wc-data.php | 69 ++++++--- includes/abstracts/abstract-wc-order.php | 2 +- .../abstracts/abstract-wc-payment-token.php | 2 +- includes/class-wc-coupon.php | 2 +- includes/class-wc-customer.php | 2 +- includes/class-wc-data-store.php | 131 ++++++++++++++++++ includes/class-wc-order-item.php | 2 +- includes/class-wc-order-refund.php | 2 +- includes/class-wc-shipping-zone.php | 2 +- .../data-stores/class-wc-data-store-cpt.php | 16 +++ .../interface-wc-object-data-store.php | 38 +++++ tests/framework/class-wc-dummy-data-store.php | 36 +++++ tests/framework/class-wc-mock-wc-data.php | 2 +- tests/unit-tests/crud/data-store.php | 104 ++++++++++++++ woocommerce.php | 5 +- 15 files changed, 387 insertions(+), 28 deletions(-) create mode 100644 includes/class-wc-data-store.php create mode 100644 includes/data-stores/class-wc-data-store-cpt.php create mode 100644 includes/data-stores/interface-wc-object-data-store.php create mode 100644 tests/framework/class-wc-dummy-data-store.php create mode 100644 tests/unit-tests/crud/data-store.php diff --git a/includes/abstracts/abstract-wc-data.php b/includes/abstracts/abstract-wc-data.php index a9fa0baf0ec..80179340b67 100644 --- a/includes/abstracts/abstract-wc-data.php +++ b/includes/abstracts/abstract-wc-data.php @@ -27,12 +27,26 @@ abstract class WC_Data { */ protected $data = array(); + /** + * Extra data for this object. Name value pairs (name + default value). + * Used as a standard way for sub classes (like product types) to add + * additional information to an inherited class. + * @var array + */ + protected $extra_data = array(); + /** * Set to _data on construct so we can track and reset data if needed. * @var array */ protected $default_data = array(); + /** + * Contains a reference to the data store for this class. + * @var object + */ + protected $data_store; + /** * Stores meta in cache for future reads. * A group must be set to to enable caching. @@ -83,31 +97,28 @@ abstract class WC_Data { return $this->id; } - /** - * Creates new object in the database. - */ - public function create() {} - - /** - * Read object from the database. - * @param int ID of the object to load. - */ - public function read( $id ) {} - /** * Updates object data in the database. */ - public function update() {} - - /** - * Updates object data in the database. - */ - public function delete() {} + public function delete( $force_delete = false ) { + if ( $this->data_store ) { + $this->data_store->delete( $this, $force_delete ); + } + } /** * Save should create or update based on object existance. */ - public function save() {} + public function save() { + if ( $this->data_store ) { + if ( $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + return $this->get_id(); + } + } /** * Change data to JSON format. @@ -125,6 +136,26 @@ abstract class WC_Data { return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) ); } + /** + * Returns array of expected data keys for this object. + * + * @since 2.7.0 + * @return array + */ + public function get_data_keys() { + return array_keys( $this->data ); + } + + /** + * Returns all "extra" data keys for an object (for sub objects like product types). + * + * @since 2.7.0 + * @return array + */ + public function get_extra_data_keys() { + return array_keys( $this->extra_data ); + } + /** * Filter null meta values from array. * @return bool @@ -400,7 +431,7 @@ abstract class WC_Data { /** * Set all props to default values. */ - protected function set_defaults() { + public function set_defaults() { $this->data = $this->default_data; } diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index 0725c9f2471..d5fdeec2c87 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -272,7 +272,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { * Delete data from the database. * @since 2.7.0 */ - public function delete() { + public function delete( $force_delete = false ) { wp_delete_post( $this->get_id() ); } diff --git a/includes/abstracts/abstract-wc-payment-token.php b/includes/abstracts/abstract-wc-payment-token.php index 5ddec88a729..5af72c8313e 100644 --- a/includes/abstracts/abstract-wc-payment-token.php +++ b/includes/abstracts/abstract-wc-payment-token.php @@ -277,7 +277,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Remove a payment token from the database. * @since 2.6.0 */ - public function delete() { + public function delete( $force_delete = false ) { global $wpdb; $this->read( $this->get_id() ); // Make sure we have a token to return after deletion $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $this->get_id() ), array( '%d' ) ); diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index d9593857ad4..6031737f221 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -752,7 +752,7 @@ class WC_Coupon extends WC_Legacy_Coupon { * Delete coupon from the database. * @since 2.7.0 */ - public function delete() { + public function delete( $force_delete = false ) { wp_delete_post( $this->get_id() ); do_action( 'woocommerce_delete_coupon', $this->get_id() ); $this->set_id( 0 ); diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php index d57040b3f04..ed513b92e71 100644 --- a/includes/class-wc-customer.php +++ b/includes/class-wc-customer.php @@ -1176,7 +1176,7 @@ class WC_Customer extends WC_Legacy_Customer { * Delete a customer. * @since 2.7.0 */ - public function delete() { + public function delete( $force_delete = false ) { if ( ! $this->get_id() ) { return; } diff --git a/includes/class-wc-data-store.php b/includes/class-wc-data-store.php new file mode 100644 index 00000000000..cfc49af20d9 --- /dev/null +++ b/includes/class-wc-data-store.php @@ -0,0 +1,131 @@ + class name. + * Example: 'product' => 'WC_Product_Data_Store_CPT' + * You can aso pass something like product_ for product stores and + * that type will be used first when avaiable, if a store is requested like + * this and doesn't exist, then the store would fall back to 'product'. + * Ran through `woocommerce_data_stores`. + */ + private $stores = array( + ); + + /** + * Contains the name of the current data store's class name. + */ + private $current_class_name = ''; + + /** + * Tells WC_Data_Store which object (coupon, product, order, etc) + * store we want to work with. + * + * @param string $object_type Name of object. + */ + public function __construct( $object_type ) { + $this->stores = apply_filters( 'woocommerce_data_stores', $this->stores ); + + // If this object type can't be found, check to see if we can load one + // level up (so if product_type isn't found, we try product). + if ( ! array_key_exists( $object_type, $this->stores ) ) { + $pieces = explode( '_', $object_type ); + $object_type = $pieces[0]; + } + + if ( array_key_exists( $object_type, $this->stores ) ) { + $store = apply_filters( 'woocommerce_' . $object_type . '_data_store', $this->stores[ $object_type ] ); + if ( ! class_exists( $store ) ) { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + $this->current_class_name = $store; + $this->instance = new $store; + } else { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + } + + /** + * Loads a data store for us or returns null if an invalid store. + * + * @param string $object_type Name of object. + * @since 2.7.0 + */ + public static function load( $object_type ) { + try { + return new WC_Data_Store( $object_type ); + } catch ( Exception $e ) { + return null; + } + } + + /** + * Returns the class name of the current data store. + * + * @since 2.7.0 + * @return string + */ + public function get_current_class_name() { + return $this->current_class_name; + } + + /** + * Reads an object from the data store. + * + * @since 2.7.0 + * @param WC_Data + */ + public function read( &$data ) { + $this->instance->read( $data ); + } + + /** + * Create an object in the data store. + * + * @since 2.7.0 + * @param WC_Data + */ + public function create( &$data ) { + $this->instance->create( $data ); + } + + /** + * Update an object in the data store. + * + * @since 2.7.0 + * @param WC_Data + */ + public function update( &$data ) { + $this->instance->update( $data ); + } + + /** + * Delete an object from the data store. + * + * @since 2.7.0 + * @param WC_Data + * @param bool $force_delete True to permently delete, false to trash. + */ + public function delete( &$data, $force_delete = false ) { + $this->instance->delete( $data, $force_delete ); + } + +} diff --git a/includes/class-wc-order-item.php b/includes/class-wc-order-item.php index 488995533eb..38c728a411c 100644 --- a/includes/class-wc-order-item.php +++ b/includes/class-wc-order-item.php @@ -249,7 +249,7 @@ class WC_Order_Item extends WC_Data implements ArrayAccess { * Delete data from the database. * @since 2.7.0 */ - public function delete() { + public function delete( $force_delete = false ) { if ( $this->get_id() ) { global $wpdb; do_action( 'woocommerce_before_delete_order_item', $this->get_id() ); diff --git a/includes/class-wc-order-refund.php b/includes/class-wc-order-refund.php index 80dd9e027db..c471db0d553 100644 --- a/includes/class-wc-order-refund.php +++ b/includes/class-wc-order-refund.php @@ -102,7 +102,7 @@ class WC_Order_Refund extends WC_Abstract_Order { * Delete data from the database. * @since 2.7.0 */ - public function delete() { + public function delete( $force_delete = false ) { wp_delete_post( $this->get_id(), true ); } diff --git a/includes/class-wc-shipping-zone.php b/includes/class-wc-shipping-zone.php index cbb569cee7c..32a25abe056 100644 --- a/includes/class-wc-shipping-zone.php +++ b/includes/class-wc-shipping-zone.php @@ -107,7 +107,7 @@ class WC_Shipping_Zone extends WC_Data { * Delete a zone. * @since 2.6.0 */ - public function delete() { + public function delete( $force_delete = false ) { if ( $this->get_id() ) { global $wpdb; $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'zone_id' => $this->get_id() ) ); diff --git a/includes/data-stores/class-wc-data-store-cpt.php b/includes/data-stores/class-wc-data-store-cpt.php new file mode 100644 index 00000000000..2d94057a2b0 --- /dev/null +++ b/includes/data-stores/class-wc-data-store-cpt.php @@ -0,0 +1,16 @@ +meta_type ) { wp_delete_user( $this->get_id() ); } else { diff --git a/tests/unit-tests/crud/data-store.php b/tests/unit-tests/crud/data-store.php new file mode 100644 index 00000000000..88ea17ab6cc --- /dev/null +++ b/tests/unit-tests/crud/data-store.php @@ -0,0 +1,104 @@ +assertEquals( $e->getMessage(), 'Invalid data store.' ); + return; + } + $this->fail( 'Invalid data store exception not correctly raised.' ); + } + + /** + * Make sure ::load returns null if an invalid store is found. + * + * @since 2.7.0 + */ + function test_invalid_store_load_returns_null() { + $product_store = WC_Data_Store::load( 'product-test' ); + $this->assertNull( $product_store ); + } + + /** + * Make sure we can swap out stores. + * + * @since 2.7.0 + */ + function test_store_swap() { + $this->load_dummy_store(); + + $store = new WC_Data_Store( 'dummy' ); + $this->assertEquals( 'WC_Dummy_Data_Store_CPT', $store->get_current_class_name() ); + + add_filter( 'woocommerce_dummy_data_store', array( $this, 'set_dummy_store' ) ); + + $store = new WC_Data_Store( 'dummy' ); + $this->assertEquals( 'WC_Dummy_Data_Store_Custom_Table', $store->get_current_class_name() ); + + add_filter( 'woocommerce_dummy_data_store', array( $this, 'set_default_dummy_store' ) ); + } + + /** + * Test to see if `first_second ``-> returns to `first` if unregistered. + * + * @since 2.7.0 + */ + function test_store_sub_type() { + $this->load_dummy_store(); + $store = WC_Data_Store::load( 'dummy_sub' ); + $this->assertEquals( 'WC_Dummy_Data_Store_CPT', $store->get_current_class_name() ); + } + + // Helper Functions + + /** + * Loads two dummy data store classes that can be swapt out for each other. Adds to the `woocommerce_data_stores` filter. + * + * @since 2.7.0 + */ + function load_dummy_store() { + include_once( dirname( dirname( dirname( __FILE__ ) ) ) . '/framework/class-wc-dummy-data-store.php' ); + add_filter( 'woocommerce_data_stores', array( $this, 'add_dummy_data_store' ) ); + } + + /** + * Adds a default class for the 'dummy' data store. + * + * @since 2.7.0 + */ + function add_dummy_data_store( $stores ) { + $stores['dummy'] = 'WC_Dummy_Data_Store_CPT'; + return $stores; + } + + /** + * Helper function/filter to swap out the default dummy store for a different one. + * + * @since 2.7.0 + */ + function set_dummy_store( $store ) { + return 'WC_Dummy_Data_Store_Custom_Table'; + } + + /** + * Helper function/filter to swap out the 'dummy' store for the default one. + * + * @since 2.7.0 + */ + function set_default_product_store( $store ) { + return 'WC_Dummy_Data_Store_CPT'; + } +} diff --git a/woocommerce.php b/woocommerce.php index 2f1dc0c9079..70e06e83a22 100644 --- a/woocommerce.php +++ b/woocommerce.php @@ -268,7 +268,6 @@ final class WooCommerce { include_once( WC_ABSPATH . 'includes/class-wc-api.php' ); // API Class include_once( WC_ABSPATH . 'includes/class-wc-auth.php' ); // Auth Class include_once( WC_ABSPATH . 'includes/class-wc-post-types.php' ); // Registers post types - include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php' ); // WC_Data for CRUD include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php' ); // Payment Tokens include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php' ); // Products include_once( WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php' ); // Orders @@ -285,6 +284,10 @@ final class WooCommerce { include_once( WC_ABSPATH . 'includes/class-wc-cache-helper.php' ); // Cache Helper include_once( WC_ABSPATH . 'includes/class-wc-https.php' ); // https Helper + include_once( WC_ABSPATH . 'includes/class-wc-data-store.php' ); // WC_Data_Store for CRUD + include_once( WC_ABSPATH . 'includes/data-stores/interface-wc-object-data-store.php' ); + include_once( WC_ABSPATH . 'includes/data-stores/class-wc-data-store-cpt.php' ); + if ( defined( 'WP_CLI' ) && WP_CLI ) { include_once( WC_ABSPATH . 'includes/class-wc-cli.php' ); }