Categories
Coding

How WooCommerce handles object data

Previously on “Stop using arrays for everything“, I exposed that an array wasn’t always the best way to accomplish a task and I gave an example in which an object was a better solution for that case.

Today, I’d like to illustrate how powerful the OOP can be and the benefits of using objects. To do so, I’ll use as an example one of the most popular WordPress plugins and the biggest e-commerce platform at this moment. I’m talking about WooCommerce and how this one handles the data of its different kinds of objects like products, orders, coupons, etc.

These e-commerce elements are represented by entity classes. E.g. The orders use the class WC_Order. The coupons, the class WC_Coupon, and so on.

All these classes have something in common, they extend the class WC_Data. And this class is, in my opinion, one of these hidden jewels included in the WooCommerce core that is worth a look at it.

You might be thinking: “I’m not working with WooCommerce, so this post is not for me”, but trust me, thanks to this class, we’re going to discover a good way for abstracting the data layer from the rest of the code. And what it’s better, we’ll be able to create our own reusable and well-tested class for handling the object data in our WordPress plugins. So, let’s start:

The class WC_Data

This class, as its own description says:

Handles generic data interaction which is implemented by the different data store classes.

WC_Data class description.

There are a few concepts here like data interaction and data store classes that are quite abstract, so instead of giving you a definition, which you can find anywhere on the Internet, we’re going to see them with an example.

When you create an object that makes use of this class, you can do things like this:

// This class extends WC_Data.
$product = new Product();

// Set some product properties.
$product->set_title( 'My awesome product' );
$product->set_price( '20' );

// The product is created in the database.
$product_id = $product->save();

// Read the product from the database.
$product_2 = new Product( $product_id );

$product_2->get_title(); // 'My awesome product'.
$product_2->set_price( '25' ); // Update the price.

// The product is updated in the database.
$product->save();

// The product is deleted from the database.
$product->delete();

Pretty cool, right? Moreover, the product object keeps track of the changes applied to it since it was created/fetched from the database. So when it’s saved, only the changes are updated, not the whole object, which makes the save process quite efficient.

But, how is it possible all of this with just a single class? Well, because the WC_Data class doesn’t work alone, it receives the help of another class, a “Data_Store” class.

In our previous example, we found two different types of methods, some of them for interacting with the object data (getters and setters), and others for the CRUDs operations (save, delete, etc.).

The getters and setters methods are handled directly by the class WC_Data. Meanwhile, the CRUD’s operations are done by the data store class.

To explain how these classes work together, we are going to create our own classes, which will be a very simplified version of the originals.

Let’s start by creating our own Data class:

abstract class Data {

    /**
	 * Object data in pairs (name + value).
	 *
	 * @var array
	 */
    protected $data = array();

    /**
	 * Data changes for this object.
	 *
	 * @var array
	 */
	protected $changes = array();


    /**
	 * Gets the value of the specified property.
     *
     * @param string $prop The property name.
     * @return mixed
     */
    protected function get_prop( $pop ) {
        $value = null;

        if ( array_key_exists( $prop, $this->data ) ) {
            $value = ( array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ] );
        }

        return $value;
    }
 
    /**
	 * Sets the value of the specified property.
     *
     * @param string $prop  The property name.
     * @param mixed  $value The value to set.
     */
    protected function set_prop( $prop, $value ) {
        if ( ! array_key_exists( $prop, $this->data ) ) {
            return;
        }

        if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
		    $this->changes[ $prop ] = $value;
	    }
    }

    /**
	 * Gets the data changes.
	 *
	 * @return array
	 */
	public function get_changes() {
		return $this->changes;
	}

    /**
	 * Merges changes with data and clear.
	 */
	public function apply_changes() {
		$this->data    = array_replace( $this->data, $this->changes ); // @codingStandardsIgnoreLine
		$this->changes = array();
	}
}

The class is pretty simple, just two properties, one for storing the object data and another for the changes. The data and the changes are represented by an array of pairs name + value. Each entry represents a property of the object that can be fetched and/or modified by the methods get_prop() and set_prop().

Some considerations about this class:

  • The methods get_prop() and set_prop() only work with properties defined in $data.
  • The method set_prop() sets the value in $changes, not in $data. This way, we keep track of the changes applied to the object. The changes won’t be merged with $data until the object is saved, when we’ll call the method apply_changes().
  • Finally, the methods get_prop() and set_prop() are protected. That means they cannot be used freely by the users.

Now, let’s make use of this class in a hypothetical Product class:

class Product extends Data {

    /**
     * Product data.
     *
     * @var array
     */
    protected $data = array(
        'name'  => '',
        'price' => '',
    );

    /**
     * Gets the product name.
	 *
	 * @return string
	 */
    public function get_name() {
        return $this->get_prop( 'name' );
    }

    /**
	 * Gets the product price.
	 *
	 * @return string
	 */
    public function get_price() {
        return $this->get_prop( 'price' );
    }

    /**
	 * Sets the product name.
	 *
	 * @param string $name Product name.
	 */
    public function set_name( $name ) {
        $this->set_prop( 'name', $name );
    }

    /**
	 * Sets the product price.
	 *
	 * @param string $price Product price.
	 */
    public function set_price( $price ) {
        $this->set_prop( 'price', floatval( $price ) );
    }
}

Our Product class has two properties, name and price, defined in $data with a default value. Each property has its own getter and setter methods.

Although this code could be quite verbose, it allows us to define which data properties we expose (make public) and which don’t. We could even define only read/write properties or properties that are not public at all. That’s why the methods get_prop() and set_prop() are protected. We don’t want to expose all $data properties by default.

By now, we have solved how to access and modify the different properties of an object, but we haven’t finished yet. We still have to implement the CRUD’s operations.

CRUD’s operations

The acronym “CRUD” is just the combination of the first letter of the words “Create”, “Read”, “Update”, and “Delete”, which are the operations we are going to implement in our Data class.

Note: To not make the following code block too long and keep the focus on the new functionality, I’m going to omit all the code implemented in this class previously.

abstract class Data {

    // Assume all the code implemented previously.

    /**
	 * Object ID.
	 *
	 * @var int
	 */
    protected $id = 0;

    /**
	 * A reference to the data store.
	 *
	 * @var Data_Store
	 */
	protected $data_store;


    /**
	 * Gets the object ID.
     *
     * @return int
     */
    public function get_id() {
        return $this->id;
    }

    /**
	 * Sets the object ID.
     *
     * @param int $id Object ID.
     */
    public function set_id( $id ) {
        $this->id = absint( $id );
    }
 
    /**
	 * Creates or updates the object.      
     *
     * @return int
     */
    public function save() {
        if ( ! $this->data_store ) {
            return $this->get_id();
        }

        if ( $this->get_id() ) {
		    $this->data_store->update( $this );
	    } else {
            $this->data_store->create( $this );
        }

        return $this->get_id();
    }

    /**
     * Deletes the object.
     *
     * @return bool
     */
    public function delete() {
        if ( ! $this->data_store ) {
            return false;
        }

        return $this->data_store->delete( $this );
    }
}

Reading the code carefully, we see the class has two new properties, $id and $data_store. The first one is self-explanatory, as we are going to create multiple instances of this object, we need a way to identify these objects.

The $data_store property is not so intuitive. By now, I can tell you that it will be an instance of a Data_Store class. This class will be in charge of persisting and recovering the object data, so it will include the logic of the CRUD’s operations.

As you can see in the methods save() and delete(), the logic of the operations is delegated to methods of the Data_Store class. This way of structuring the code is known as the Strategy Pattern, and it allows us to decouple the code of the CRUD’s operations from the Data class.

The data store classes

In WooCommerce, each type of object (product, order, etc.) has its own data store class (a concrete class in the strategy pattern), and the way they persist and fetch the data can be totally different.

The best part of using data store classes is that you could change the implementation of the CRUD’s operations of an object just by replacing its data store class and without the need of modifying the rest of the code. This is especially useful when you need to migrate the data from one storage system to another.

E.g: Currently, WooCommerce stores the product objects in the WordPress “posts” and “postmetas” tables. If anytime the development team would like to create specific (and more optimized) tables for this kind of content, they only would need to modify the WC_Product_Data_Store_CPT class and everything would continue working like before. You wouldn’t notice the change unless you interacted with the product data directly in the database (Don’t do it).

This interchangeability is only possible if the data store classes use the same methods for the CRUD’s operations, so the next step is to create an interface as a contract for all of them.

The following fragment of code is a simplified version of the WooCommerce interface WC_Object_Data_Store_Interface.

interface Data_Store {

    /**
	 * Creates a Data based object.
	 *
	 * @param Data $object Data object.
	 */
	public function create( &$object );

	/**
	 * Reads a Data based object.
	 *
	 * @param Data $object Data object.
	 */
	public function read( &$object );

	/**
	 * Updates a Data based object.
	 *
	 * @param Data $object Data object.
	 */
	public function update( &$object );

	/**
	 * Deletes a Data based object.
	 *
	 * @param Data $object Data object.
	 * @return bool
	 */
	public function delete( &$object );
}

Notice how all the methods receive an instance (by reference) of the object they will interact with. Any change applied to this object will be reflected immediately in the original.

Now, it’s time to create a specific data store class for our Product class by making use of this interface. In our implementation, we are going to store the data in the WordPress tables “posts” and “postmeta“.

class Product_Data_Store implements Data_Store {

    /**
	 * Creates a new product.
	 *
	 * @param Product $product Product object.
	 */
	 public function create( &$product ) {
         $data = array(
             'post_type'    => 'product',
             'post_title'   => $product->get_name(),
             'post_content' => '',
         );

         $product_id = wp_insert_post( $data, true );

         if ( $product_id && ! is_wp_error( $product_id ) ) {
             $product->set_id( $product_id );

             // Save the rest of properties as metadata.
             add_post_meta( $product_id, 'price', $product->get_price() );

             $product->apply_changes();
         }
     }
}

As you can see, the method create() receives a Product object and stores it into the database by creating a new post with the function wp_insert_post(). The “posts” table has a restricted set of columns like ‘post_title’, ‘post_content’, etc., so the rest of the product data needs to be stored as metadata.

Once the post is inserted, we update the product ID with the returned post ID. Finally, we apply the changes to the object data and reset the changes. This way, our product object is ready to log new changes in its data.

Let’s continue with the read() method:

/**
 * Reads a product.
 *
 * @param Product $product Product object.
 */
public function read( &$product ) {
    $product_id = $product->get_id();
    $post       = ( $product_id ? get_post( $product_id ) : null );

    if ( ! $post || 'product' !== $post->post_type ) {   
        throw new Exception( 'Invalid product.' );
    }

    $product->set_name( $post->post_title );
    $product->set_price( get_post_meta( $post_id, 'price', true );
}

This method is quite simple, just fetches the post object with the function get_post() and checks the returned object is a product. Otherwise, it throws an Exception. Finally, it populates the product data by calling the setter methods.

Now, the update() method:

/**
 * Updates a product.
 *
 * @param Product $product Product object.
 */
public function update( &$product ) {
    $product_id = $product->get_id();
    $changes    = $progressive_discount->get_changes();

    // Update post object.
	$post_data = array(
        'ID'                => $product_id,
        'post_title'        => $product->get_name(),
		'post_modified'     => current_time( 'mysql' ),
		'post_modified_gmt' => current_time( 'mysql', 1 ),
	);

    wp_update_post( $post_data );

    // Update post metas.
    foreach ( $changes as $prop => $value ) {
        if ( 'name' === $prop ) {
            continue;
        }

        // Sanitize value here.

        update_post_meta( $product_id, $prop, $value );
    }

    $product->apply_changes();
}

The update method is a bit more complex. We use the function wp_update_post() to update the product data in the “posts” table, including the “post_modified” columns.

Next, we update the post metadata, but only for the modified properties. And finally, we apply the changes to the product data.

And the last one, the delete() method:

/**
 * Deletes a product.
 *
 * @param Product $product Product object.
 * @return bool
 */
public function delete( &$product ) {
    $product_id = $product->get_id();

    if ( ! $product_id ) {
        return false;
    }

    wp_delete_post( $product_id, true );

    $product->set_id( 0 );

    return true;
}

This is the easiest method. First, it checks the product is not new (it has an ID), then uses the function wp_delete_post() to delete the object from the database, and finally, set the product ID to zero to reflect its new status.

All together

Now that our Product class has its own data store class, it’s time to do the final tweaks to make them work together.

class Product extends Data {

    // Assume all the code implemented previously.

    /**
     * Constructor.
	 *
	 * @param int $product_id Optional. Product ID.
	 */
    public function __construct( $product_id = 0) {
        $this->data_store = new Product_Data_Store();

        if ( is_numeric( $product ) && $product > 0 ) {
            $this->set_id( $product_id );
            $this->data_store->read( $this );
        }
    }
}

The only missing functionality is the constructor, in which we initialize the $data_store property with an instance of Product_Data_Store and use its method read() for fetching the data from the database in case a product ID is provided.

Note: I know the classes Product and Product_Data_Store are quite coupled at this moment, but this is just an example of implementation. You could inject the data store class as a dependency or use a different approach for decoupling them.

We are close to the end. Let’s see how it looks like our classes with all the code:

abstract class Data {

    /**
	 * Object ID.
	 *
	 * @var int
	 */
    protected $id = 0;

    /**
	 * A reference to the data store.
	 *
	 * @var Data_Store
	 */
    protected $data_store;

    /**
	 * Object data in pairs (name + value).
	 *
	 * @var array
	 */
    protected $data = array();

    /**
	 * Data changes for this object.
	 *
	 * @var array
	 */
	protected $changes = array();


    /**
	 * Gets the object ID.
     *
     * @return int
     */
    public function get_id() {
        return $this->id;
    }

    /**
	 * Sets the object ID.
     *
     * @param int $id Object ID.
     */
    public function set_id( $id ) {
        $this->id = absint( $id );
    }
 
    /**
	 * Gets the value of the specified property.
     *
     * @param string $prop The property name.
     * @return mixed
     */
    protected function get_prop( $pop ) {
        $value = null;

        if ( array_key_exists( $prop, $this->data ) ) {
            $value = ( array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ] );
        }

        return $value;
    }
 
    /**
	 * Sets the value of the specified property.
     *
     * @param string $prop  The property name.
     * @param mixed  $value The value to set.
     */
    protected function set_prop( $prop, $value ) {
        if ( ! array_key_exists( $prop, $this->data ) ) {
            return;
        }

        if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
		    $this->changes[ $prop ] = $value;
	    }
    }

    /**
	 * Gets the data changes.
	 *
	 * @return array
	 */
	public function get_changes() {
		return $this->changes;
	}

    /**
	 * Merges changes with data and clear.
	 */
	public function apply_changes() {
		$this->data    = array_replace( $this->data, $this->changes ); // @codingStandardsIgnoreLine
		$this->changes = array();
	}

    /**
	 * Creates or updates the object.      
     *
     * @return int
     */
    public function save() {
        if ( ! $this->data_store ) {
            return $this->get_id();
        }

        if ( $this->get_id() ) {
		    $this->data_store->update( $this );
	    } else {
            $this->data_store->create( $this );
        }

        return $this->get_id();
    }

    /**
     * Deletes the object.
     *
     * @return bool
     */
    public function delete() {
        if ( ! $this->data_store ) {
            return false;
        }

        return $this->data_store->delete( $this );
    }
}
class Product extends Data {

    /**
     * Product data.
     *
     * @var array
     */
    protected $data = array(
        'name'  => '',
        'price' => '',
    );


    /**
     * Constructor.
	 *
	 * @param int $product_id Optional. Product ID.
	 */
    public function __construct( $product_id = 0) {
        $this->data_store = new Product_Data_Store();

        if ( is_numeric( $product ) && $product > 0 ) {
            $this->set_id( $product_id );
            $this->data_store->read( $this );
        }
    }

    /**
     * Gets the product name.
	 *
	 * @return string
	 */
    public function get_name() {
        return $this->get_prop( 'name' );
    }

    /**
	 * Gets the product price.
	 *
	 * @return string
	 */
    public function get_price() {
        return $this->get_prop( 'price' );
    }

    /**
	 * Sets the product name.
	 *
	 * @param string $name Product name.
	 */
    public function set_name( $name ) {
        $this->set_prop( 'name', $name );
    }

    /**
	 * Sets the product price.
	 *
	 * @param string $price Product price.
	 */
    public function set_price( $price ) {
        $this->set_prop( 'price', floatval( $price ) );
    }
}
class Product_Data_Store implements Data_Store {

    /**
	 * Creates a new product.
	 *
	 * @param Product $product Product object.
	 */
	 public function create( &$product ) {
         $data = array(
             'post_type'    => 'product',
             'post_title'   => $product->get_name(),
             'post_content' => '',
         );

         $product_id = wp_insert_post( $data, true );

         if ( $product_id && ! is_wp_error( $product_id ) ) {
             $product->set_id( $product_id );

             // Save the rest of properties as metadata.
             add_post_meta( $product_id, 'price', $product->get_price() );

             $product->apply_changes();
         }
     }

    /**
     * Reads a product.
     *
     * @param Product $product Product object.
     */
    public function read( &$product ) {
        $product_id = $product->get_id();
        $post       = ( $product_id ? get_post( $product_id ) : null );

        if ( ! $post || 'product' !== $post->post_type ) {
            throw new Exception( 'Invalid product.' );
        }

        $product->set_name( $post->post_title );
        $product->set_price( get_post_meta( $post_id, 'price', true );
    }

    /**
     * Updates a product.
     *
     * @param Product $product Product object.
     */
    public function update( &$product ) {
        $product_id = $product->get_id();
        $changes    = $progressive_discount->get_changes();

        // Update post object.
        $post_data = array(
            'ID'                => $product_id,
            'post_title'        => $product->get_name(),
		    'post_modified'     => current_time( 'mysql' ),
		    'post_modified_gmt' => current_time( 'mysql', 1 ),
        );

        wp_update_post( $post_data );

        // Update post metas.
        foreach ( $changes as $prop => $value ) {
            if ( 'name' === $prop ) {
                continue;
            }

            // Sanitize value here.

            update_post_meta( $product_id, $prop, $value );
        }

        $product->apply_changes();
    }

    /**
     * Deletes a product.
     *
     * @param Product $product Product object.
     * @return bool
     */
    public function delete( &$product ) {
        $product_id = $product->get_id();

        if ( ! $product_id ) {
            return false;
        }

        wp_delete_post( $product_id, true );

        $product->set_id( 0 );

        return true;
    }
}

In conclusion

Today, we’ve seen a good example of how to handle the data of different objects thanks to the WooCommerce data and data store classes. Obviously, these classes are much more complex and they have more functionalities like better metadata management, data cache, etc., but I think our simplified version is a good starting point to understand how WooCommerce handles the object data and I hope this post helps you to dig into its code and unravel all its secrets, and who knows, to create your own Data class in your next project. That’s all for now, see you in the next post.

Juan José

By Juan José

Building amazing products for WordPress & WooCommerce since 2012.