For the last couple weeks, I’ve talked about creating database abstractions. In the first article, I spoke about the need for creating a high-level API, on top of standard WordPress APIs to act as a CRUD interface for your projects. The second was about using classes with all static methods for validation and storage of options. In part three, I want to talk about dependency injection and illustrate the value of this concept by offering a different way to create a database abstraction than I did before.
What I showed in the last article works for its purpose. But, that system was strongly coupled to the options API and required a new class declaration for each option. Because of the use of class inheritance, there is little redundancy in code, but that approach has limited usage.
What Is Dependency Injection
Before we begin, we must go over what a dependency injection is.
Basically, it is a fancy sounding term that just means that a class takes the data it needs to function from outside of the class, instead of constructing this data itself. Dependency injections are helpful for several reasons.
A dependency injection encourages following the single responsibility principle. You might think “this class outputs popular posts in the footer of a post” when it actually assembles a WP_Query object based on the current HTTP request, queries the database, and outputs some HTML on the_content filter.
That’s a lot of responsibilities for one class. Here is the kind of class I’m talking about:
class popular_posts { public function __construct() { if ( ! is_admin() && is_single() ) { add_filter( 'the_content', [ $this, 'output' ] ); } } public function content( $content ){ $posts = $this->query()->posts; ob_start(); include 'path/to/view.php'; $view = ob_get_clean(); return $content . $view; } protected function post_type(){ return get_post_type(); } protected function query_args(){ return [ 'orderby' => 'comment_count', 'post_type' => $this->post_type(), 'posts_per_page' => 5 ]; } protected function query(){ return new WP_Query( $this->query_args() ); } } new popular_posts;
This class does not use dependency injection. As a result, it’s basically impossible to write a unit test for this class. Also, if you wanted to add AJAX based pagination, you’d be in trouble. If you wanted to reuse this class with a different view or post type, you’d have to rewrite it quite a bit. What if you wanted to use the same query, but return the posts as JSON?
This is the beauty of dependency injection. We can inject our query arguments and the view path into this class. Then we can take the HTML it generates and use that as a dependency of another class. This gives us a modular, testable, and reusable system.
Here is a refactored example of this class that takes the WP_Query arguments and the path for the view file as dependencies:
class popular_posts { protected $html; protected $query_args; protected $view_path; public function __construct( array $query_args, $view_path ) { $this->args = $query_args; $this->view_path = $view_path; } public function get_html(){ return $this->html; } public function make_html(){ $posts = $this->query()->post; ob_start(); include $this->view_path; $view = ob_get_clean(); return $view; } protected function query(){ return new WP_Query( $this->query_args ); } }
Now this class can be used with different query arguments or layouts. We just make a new instance with different values passed to the constructor. Notice that this class does not add any hooks. The HTML property can be accessed by another class whose responsibility is to hook to the_content filter.
This is an example of constructor dependency injection because the dependencies are injected in the constructor. We could also have added a public set_query() method that took a WP_Query object or an array of WP_Query arguments. That would be an example of method injection.
The third type of dependency injection is property injection. In this case, class properties are intentionally left public to allow for dependencies to be set on them externally. This is probably the least useful form of dependency injection because it can lead to the wrong type of dependency being injected.
I normally prefer constructor dependency injection. In some cases, a class needs an array of objects of a specific class. In that case, method injection is better, as you can take one object at a time and ensure it is correct using type hinting or other validation.
Let’s start with an example database abstraction for data stored as posts and posts meta:
abstract class PostType_Store { /** * Post object for queried post * * @var WP_Post */ protected $post; /** @var string */ protected $name; /** @var string */ protected $title; /** @var string */ protected $content; /** * Create object * * @param int $id Post ID. Optional. If ID is passed, post will be queried and set in post property */ public function __construct( $id = 0 ){ if( $id > 0 ){ $this->post = get_post( $id ); } } /** * Get a property of this object if public * * @param $prop * * @return mixed */ public function __get( $prop ){ if( $this->public_prop( $prop ) ){ if( ! isset( $this->$prop ) && isset( $this->post ) ) { $this->$prop = $this->post->$prop; } return $this->$prop; } } /** * Set property if public * * @param string $prop Name of property * @param mixed $value Value to set * * @return mixed */ public function __set( $prop, $value ){ if( $this->public_prop( $prop ) ){ return $this->$prop = $value; } } /** * Get the post data as a wp_insert_post compatible array. * * @return array */ public function get_post_data() { $post_data = array( 'post_title' => $this->title, 'post_status' => 'publish', 'post_type' => $this->post_type_name() ); if( false != $this->content ){ $post_data[ 'post_content' ] = $this->content; } return $post_data; } /** * Save the post * * @return int */ public function save(){ $data = $this->get_post_data(); if ( property_exists( $this, 'name' ) ) { if ( empty( $this->name ) ) { $this->name = $this->title; } $data[ 'post_name' ] = $this->name; } if ( ! is_object( $this->post ) ) { $post_id = wp_insert_post( $data, false ); }else{ $data[ 'ID' ] = $this->post->ID; $post_id = wp_update_post( $data, false ); } if ( 0 === $post_id || $post_id instanceof WP_Error ) { return $post_id; } foreach ($this->get_post_meta() as $key => $value) { update_post_meta($post_id, $key, $value); } $this->post = get_post( $post_id ); return $post_id; } /** * Get the meta keys for this post type * * @return array */ public function get_meta_keys(){ return array_keys( $this->get_post_meta() ); } /** * Get Post ID * * @return int */ public function get_id(){ if( is_object( $this->post ) ){ return $this->post->ID; } } /** * Get post object * * @return null|\WP_Post */ public function get_post(){ return $this->post; } /** * Set properties using post object */ public function reset_props(){ $this->set_meta_properties(); $this->set_post_properties(); } /** * Set meta properties using post object */ public function set_meta_properties(){ if( is_object( $this->post ) ) { foreach( $this->get_meta_keys() as $key ){ $this->$key = $this->post->$key; } } } /** * Set post properties using post object */ public function set_post_properties(){ if( is_object( $this->post ) ) { foreach( array( 'name', 'content', 'title' ) as $prop ){ if( property_exists( $this, $prop ) ){ $post_prop = 'post_'.$prop; $this->$prop = $this->post->$post_prop; } } } } /** * Checks if is a property we should allow to be set via magic set/get * * @param string $prop Name or property * * @return bool */ protected function public_prop( $prop ) { if ( is_string( $prop ) && in_array( $prop, $this->get_meta_keys() ) || in_array( $prop, array( 'title', 'content' ) ) ) { return true; } if( 'name' == $prop && property_exists( $this, 'name' ) ){ return true; } } /** * In subclass set an array of meta keys used for this object * * IMPORTANT: Create properites for each field * * array should be 'key_name' => $property * * @return array */ abstract protected function get_post_meta(); /** * Define the name of the post type * * @return mixed */ abstract protected function post_type_name(); }
This class makes use of both constructor dependency injection and method dependency injection. All of the class properties can be set via the magic __get() method, and retrieved via the magic __set() method. We inject the ID of the post into the constructor and let the class assemble the data we need from post and post_meta.
Here is an example class that extends this to implement the system:
class items extends PostType_Store { protected $size; protected $year; /** * @inheritdoc */ protected function get_post_meta(){ return [ 'size' => $this->size, 'year' => $this->year, ]; } /** * @inheritdoc */ protected function post_type_name(){ return 'slug_items'; } }
This code is still completely tied to the way that the WP_Post class functions and stores data. I’m OK with that in this case because post and post meta storage is pretty specific to WordPress and hard to escape without moving to custom tables.
Despite the strong coupling I still like this because it makes the WordPress APIs invisible outside the encapsulation of this class. Look at this example of how we would use our example implementation:
//create a new item $big_item = new items(); $big_item->size = 'large'; $big_item->year = '2008'; $big_item->title = 'Big old 2008 thing'; $big_item->save(); //get size of saved item $true_item = new items( 42 ); echo $true_item->size;
In this code I used various properties of the class, via the magic __get() and __set() methods to create a new item in the database as well as to query for and echo data. The data is stored in a post, which is invisible to the outside world.
If I wrote a plugin using this system and in a later version to store data as posts and then later I switched to storing data in custom tables, that code would not break. Yes, I’d need to rewrite the internals of the class and create a migration system. But, all the code using that database abstraction would still function the same way.
The Power Of Invisibility
In my first article in this series, I talked about the reason behind making your own database abstractions. A good database abstraction makes the API that interfaces with WordPress and by extension the database invisible. By making these APIs “invisible” the way they function becomes irrelevant to the outside user.
A good database abstraction is essentially a black box. Data goes in, data goes out and we don’t really know why or how. We don’t need to know why and how. It just works and we can build on top of it any way we want to.
I hope in this article and the last two you have shown the value of creating database abstractions on top of WordPress APIs. The sooner you adopt a database abstraction in your work, the more stable your project will be and the easier it will be to make a change to how you store data in the future. If you want to replace options or post/ post meta storage with custom table storage, then that will be easy. If you do, be sure to read Pippin Williamson’s excellent series on creating custom table APIs that begins here.
No Comments