Developing using WooCommerce CRUD objects
CRUD is an abbreviation of the four basic operations you can do to a database or resource – Create, Read, Update, Delete.
WooCommerce 3.0 introduced CRUD objects for working with WooCommerce data. Whenever possible these objects should be used in your code instead of directly updating metadata or using WordPress post objects.
Each of these objects contains a schema for the data it controls (properties), a getter and setter for each property, and a save/delete method which talks to a data store. The data store handles the actual saving/reading from the database. The object itself does not need to be aware of where the data is stored.
The benefits of CRUD
- Structure – Each object has a pre-defined structure and keeps its own data valid.
- Control – We control the flow of data, and any validation needed, so we know when changes occur.
- Ease of development – As a developer, you don’t need to know the internals of the data you’re working with, just the names.
- Abstraction – The data can be moved elsewhere, e.g. custom tables, without affecting existing code.
- Unification – We can use the same code for updating things in admin as we do in the REST API and CLIs. Everything is unified.
- Simplified code – Less procedural code to update objects which reduces likelihood of malfunction and adds more unit test coverage.
CRUD object structure
The WC_Data
class is the basic implementation for CRUD objects, and all CRUD objects extend it. The most important properties to note are $data
, which is an array of props supported in each object, and $id
, which is the object’s ID.
The coupon object class is a good example of extending WC_Data
and adding CRUD functions to all properties.
Data
$data
stores the property names, and default values:
/**
* Data array, with defaults.
* @since 3.0.0
* @var array
*/
protected $data = array(
'code' => '',
'amount' => 0,
'date_created' => '',
'date_modified' => '',
'discount_type' => 'fixed_cart',
'description' => '',
'date_expires' => '',
'usage_count' => 0,
'individual_use' => false,
'product_ids' => array(),
'excluded_product_ids' => array(),
'usage_limit' => 0,
'usage_limit_per_user' => 0,
'limit_usage_to_x_items' => 0,
'free_shipping' => false,
'product_categories' => array(),
'excluded_product_categories' => array(),
'exclude_sale_items' => false,
'minimum_amount' => '',
'maximum_amount' => '',
'email_restrictions' => array(),
'used_by' => array(),
);
Getters and setters
Each one of the keys in this array (property) has a getter and setter, e.g. set_used_by()
and get_used_by()
. $data
itself is private, so the getters and setters must be used to access the data.
Example getter:
/**
* Get records of all users who have used the current coupon.
* @since 3.0.0
* @param string $context
* @return array
*/
public function get_used_by( $context = 'view' ) {
return $this->get_prop( 'used_by', $context );
}
Example setter:
/**
* Set which users have used this coupon.
* @since 3.0.0
* @param array $used_by
* @throws WC_Data_Exception
*/
public function set_used_by( $used_by ) {
$this->set_prop( 'used_by', array_filter( $used_by ) );
}
set_prop
and get_prop
are part of WC_Data
. These apply various filters (based on context) and handle changes, so we can efficiently save only the props that have changed rather than all props.
A note on $context
: when getting data for use on the front end or display, view
context is used. This applies filters to the data so extensions can change the values dynamically. edit
context should be used when showing values to edit in the backend, and for saving to the database. Using edit
context does not apply any filters to the data.
The constructor
The constructor of the CRUD objects facilitates the read from the database. The actual read is not done by the CRUD class, but by its data store.
Example:
/**
* Coupon constructor. Loads coupon data.
* @param mixed $data Coupon data, object, ID or code.
*/
public function __construct( $data = '' ) {
parent::__construct( $data );
if ( $data instanceof WC_Coupon ) {
$this->set_id( absint( $data->get_id() ) );
} elseif ( is_numeric( $data ) && 'shop_coupon' === get_post_type( $data ) ) {
$this->set_id( $data );
} elseif ( ! empty( $data ) ) {
$this->set_id( wc_get_coupon_id_by_code( $data ) );
$this->set_code( $data );
} else {
$this->set_object_read( true );
}
$this->data_store = WC_Data_Store::load( 'coupon' );
if ( $this->get_id() > 0 ) {
$this->data_store->read( $this );
}
}
Note how it sets the ID based on the data passed to the object, then calls the data store to retrieve the data from the database. Once the data is read via the data store, or if no ID is set, $this->set_object_read( true );
is set so the data store and CRUD object knows it’s read. Once this is set, changes are tracked.
Saving and deleting
Save and delete methods are optional on CRUD child classes because the WC_Data
parent class can handle it. When save
is called, the data store is used to store data to the database. Delete removes the object from the database. save
must be called for changes to persist, otherwise they will be discarded.
The save method in WC_Data
looks like this:
/**
* Save should create or update based on object existence.
*
* @since 2.6.0
* @return int
*/
public function save() {
if ( $this->data_store ) {
// Trigger action before saving to the DB. Allows you to adjust object props before save.
do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
if ( $this->get_id() ) {
$this->data_store->update( $this );
} else {
$this->data_store->create( $this );
}
return $this->get_id();
}
}
Update/create is used depending on whether the object has an ID yet. The ID will be set after creation.
The delete method is like this:
/**
* Delete an object, set the ID to 0, and return result.
*
* @since 2.6.0
* @param bool $force_delete
* @return bool result
*/
public function delete( $force_delete = false ) {
if ( $this->data_store ) {
$this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
$this->set_id( 0 );
return true;
}
return false;
}
CRUD usage examples
Creating a new simple product
$product = new WC_Product_Simple();
$product->set_name( 'My Product' );
$product->set_slug( 'myproduct' );
$product->set_description( 'A new simple product' );
$product->set_regular_price( '9.50' );
$product->save();
$product_id = $product->get_id();
Updating an existing coupon
$coupon = new WC_Coupon( $coupon_id );
$coupon->set_discount_type( 'percent' );
$coupon->set_amount( 25.00 );
$coupon->save();
Retrieving a customer
$customer = new WC_Customer( $user_id );
$email = $customer->get_email();
$address = $customer->get_billing_address();
$name = $customer->get_first_name() . ' ' . $customer->get_last_name();
Last updated: February 07, 2024