Being a plugin developer isn’t easy. You have to get your plugin to work with WordPress. But more often than not, you also need it to interact with other plugins.
This interaction can take various forms. For example, you might need to modify another plugin’s behaviour using the plugin API. Or you might want to help customers migrate away from another plugin (or product) to yours.
This second scenario is the one that we’re going to look at in this article. It’s a good opportunity to introduce a new software design pattern. We call it the strategy pattern.
What is the strategy pattern?
So what is this strategy pattern and what’s so great about it? Well, let’s go back to the migration scenario that we want to look at. A common way of migrating someone away from another plugin is to create an importer.
In practice, you can break the job of an importer into two smaller jobs. First, it takes data from the other plugin and converts it into data that your plugin can use. Once it’s done that, it saves that converted data into the WordPress database.
Now, let’s say that you wanted to create classes for these importers. Your first instinct might be to create a class for each importer. So you’d have PluginAImporter
, PluginBImporter
and so on.
But let’s take a step back and think about this for a moment. What’s the difference between PluginAImporter
and PluginBImporter
? It’s only the first of the two jobs of the importer.
The code for fetching and converting data from “plugin A” isn’t going to be the same as the code for “plugin B”. But the code for saving the converted data is the opposite. It’s always going to be the same for both plugins.
In fact, we can go one step further. We can say that the converting code doesn’t care where the data came from. As long as the data follows the same standard, the origin of that data doesn’t matter.
How does the strategy pattern help us?
Alright, this is all nice, but where does the strategy pattern fit in all this? Well, the strategy pattern solves this type of problem. It offers a way to decouple these two jobs from one another. Here’s a diagram to illustrate how the pattern works:
This diagram has three distinct components. We have the context, the strategy interface and the concrete strategies. These all represents elements of our earlier story.
The context is the class that we’re going split into pieces. It’s our PluginAImporter
and PluginBImporter
classes from earlier. The big difference is that we don’t distinguish between the two plugins anymore.
Instead, this distinction between plugins is going to move into separate strategies. These strategies represent the variable part of our context. And the strategy interface defines the contract that our context expects each strategy to follow.
This is the part of our story where we mention that the origin of the data doesn’t matter. As long as it follows a standard, the importer will be able to save the data to the WordPress database. Well, the strategy interface is that standard.
The concrete strategies are the implementations of our strategy interface. The code for fetching and converting data from “plugin A” is one concrete strategy. The code for “plugin B” is another.
Building our importer
Now that we’ve gone over the strategy pattern, let’s look at how we can use it in practice. We’ve already done a good job at breaking down our importer into the components that the pattern uses. We just need to code them now!
A small preface
Before we begin, there’s something that you should know. The importer that we’re going to build is just going to import data from files. These files will follow a simple formatting scheme.
That’s because the goal of the example is to show how the strategy pattern works. It’s not about showing how to import complex data sets. This is a challenging problem in its own right. That’s why we’re not going to look into it in the context of this article.
Initial building blocks
Let’s start building our importer with what we know so far. We’re going to need a class that acts as our context. We’ll call it Importer
.
class Importer { }
We’ve left it empty for now, but this is going to be the class in charge of importing data from another plugin. But what else do we need? Well, we also need our strategy interface.
interface DataConverterInterface { }
Like we mentioned earlier, how we convert our data is the variable part of our context. That’s why we named our strategy interface DataConverterInterface
. We’ve also left it empty until we can clarify what we want our strategies to do.
That said, we can flesh out our Importer
class a bit more now. We know that our Importer
class will need a strategy to work. So let’s add it as a dependency to the class.
class Importer { /** * The data converter used by the importer. * * @var DataConverterInterface */ private $converter; /** * Constructor. * * @param DataConverterInterface $converter */ public function __construct(DataConverterInterface $converter) { $this->converter = $converter; } }
As you can see above, we added a constructor to our Importer
class. It accepts a DataConverterInterface
object as a parameter. That object then gets assigned to the converter
internal variable.
Importing our data
The next thing that we want to look at is how importer is going to import our data. We’ll start building out the functionality for it. This way we can see where our converter interface fits in the importing process.
class Importer { // ... /** * Import a data file. * * @param string $filename * * @return bool|WP_Error */ public function import($filename) { if (!is_readable($filename)) { return new WP_Error('file_unreadable', sprintf('Unable to read "%" data file.', $filename)); } // ... return true; } }
As we mentioned earlier, the importer is always going to import data from a file. That’s why we have filename
as a parameter for our import_from_file
method. It’s the path to the file that we want our plugin to import.
Nonetheless, we can’t go ahead and import the file just yet. We need to check if the given filename
is valid. Otherwise, our importer won’t be able to import anything!
That’s why our import_from_file
method has a guard clause. It uses the is_readable
function to check if the file exists and we can read it. If is_readable
returns false
, we return a new WP_Error
object with an error message.
But we can’t just return WP_Error
when there’s an error. We also need to return something when the import is successful. That’s why our import_from_file
method ends with a return true;
statement.
Reading the data file
Once passed our guard clause, we know that we can read the data file that we want to import. The next step is to begin the import process itself. Which brings us to an important design decision, “who’s in charge of reading the data file?”
We have two possible choices here. We could make it part of the DataConverterInterface
contract. Or we could have the Importer
class take care of it. In this article, we’re going to go with the later option.
This goes back to our earlier point. We want to focus on the strategy pattern and not how to handle the data. In the case that you want to import complex data, it might be necessary to have each strategy read the file.
class Importer { // ... /** * Import a data file. * * @param string $filename * * @return bool|WP_Error */ public function import_from_file($filename) { if (!is_readable($filename)) { return new WP_Error('file_unreadable', sprintf('Unable to read "%" data file.', $filename)); } $import_data = file($filename, FILE_IGNORE_NEW_LINES); if (!is_array($import_data)) { return new WP_Error('parse_error', sprintf('Failed to parse the "%" data file.', $filename)); } // ... return true; } }
Above is our updated our import_from_file
method. We added a bit more code to read the data in filename
into an array. This is easy to do in PHP with the file
function.
The way the function does that is pretty straightforward. Each line in your file will be an array element in the returned array. So, for example, the first line of our data file will be at import_data[0]
.
You might have also noticed that we pass the FILE_IGNORE_NEW_LINES
constant as a second argument. This is a special flag for the file
function. It tells the file
function to remove the newline characters at the end of each array element. We don’t need these when importing data.
That said, it’s still possible for errors to occur. If file
fails to generate an array from our file, it will return false
. That’s why we have another guard clause after reading the file.
What are we importing?
At this point, we’re passed the second guard clause. We know beyond a doubt that import_data
is valid. But we haven’t talked about what’s inside import_data
! (Oups!)
That’s because we’ve been focusing so much on this Importer
class that we’re building. But we haven’t talked much about the imaginary plugin that we’re building it for. So let’s take a small break and go over what we’re going to import for this imaginary plugin.
It’s customer data
So what data is our plugin importing!? Well, we won’t be too original and import customers into our plugin. But first, let’s take a moment and update the names of our class and interface to reflect this!
class CustomerImporter { // ... } interface CustomerDataConverterInterface { // ... }
We added the Customer
prefix to both our class and interface. This clarifies their role to anyone reading your code. This is important. You always want to use clear and concise names for your classes and interfaces.
class CustomerImporter { // ... /** * Import a customer data file. * * @param string $filename * * @return bool|WP_Error */ public function import_from_file($filename) { if (!is_readable($filename)) { return new WP_Error('file_unreadable', sprintf('Unable to read "%" customer data file.', $filename)); } $customer_data = file($filename, FILE_IGNORE_NEW_LINES); if (!is_array($customer_data)) { return new WP_Error('parse_error', sprintf('Failed to parse the "%" customer data file.', $filename)); } // ... return true; } }
We also want to do a clarification pass to our import_from_file
method. We want to mention that it imports a customer data file in the PHPDoc and the error messages. We also renamed the import_data
variable to customer_data
.
Customer data inside WordPress
Let’s dive in further into our customer data. How do we store it inside WordPress? For this, we’ll use a custom post type called customer
.
We’ll use the customer’s name as the post_title
. We’ll store the customer details in the post_content
field. And we’ll use a post meta to store the customer’s email address.
Again, this isn’t too complex by choice. We don’t want the exercise of managing the customer data to overwhelm us. But it’s possible to make this more complex by using a dedicated class for the customer and/or a repository.
Adding a customer
Now that we’ve explained how we’ll handle customers inside WordPress. We need code to create them. That way our importer will be able to save the new customers as it imports them.
class CustomerImporter { // ... /** * Add a new customer into the WordPress database. * * @param string $name * @param string $email * @param string $details * * @return int|WP_Error */ private function add_customer($name, $email, $details = '') { $customer_id = wp_insert_post(array( 'post_title' => $name, 'post_content' => $details, 'post_status' => 'publish', 'post_type' => 'customer' ), true); if ($customer_id instanceof WP_Error) { return $customer_id; } update_post_meta($customer_id, 'email', $email); return $customer_id; } }
Above is the add_customer
method that we added to the CustomerImporter
class. It’s in charge of saving new customers to the WordPress database. We made it private
because it’s only meant for internal use.
The method has three parameters: name
, email
and details
. These are three customer fields that we decided to use earlier. We made details
optional, but the other two parameters are mandatory.
Let’s look at the add_customer
method now. First, we create the new customer using wp_insert_post
. We pass it an array with all the necessary data to create our customer.
The array contains four key-value pairs. The first two keys are post_title
and post_content
. We assign them the value of two of our method arguments: name
and details
.
The third key is post_status
. We set it the value of publish
. That’s because the default value for post_status
is draft
. And we don’t want to create our customers as drafts. (That’d be rude!)
The last key is post_type
. It’s what we use to tell WordPress this is a custom post type. We set it to customer
which is the custom post type that our plugin created for customers.
We store the return value from wp_insert_post
inside the customer_id
variable. We then want to check to see if it’s a WP_Error
object. We can do that because we passed true
as the second argument of wp_insert_post
. This tells wp_insert_post
to return a WP_Error
object when there’s an error.
Once we’re sure that customer_id
isn’t a WP_Error
object, we can set the customer’s email address. We use update_post_meta
to do that. We pass it the customer_id
as the post ID, email
as the post meta key and the email
argument as post meta value.
We finish off by returning customer_id
. This is an optional step. We won’t need it in our code, but it’s always a good idea to return a value with this type of method.
Converting our customer data file
Alright, so we finished our small tangent. We explained that our importer was importing customer data. And we also created our add_customer
method to create these customers inside the WordPress database.
We can now go back to our import_from_file
method. We have all the information and tools that we need to wrap up the code for that method. Let’s start by going back to our strategy interface.
interface CustomerDataConverterInterface { /** * Convert the given data array into an array of customers. The returned * array should use the following format: * * array( * array( * 'name' => (string) Name of the customer. * 'email' => (string) Email address of the customer. * 'details' => (string) The customer details. * ) * ) * * @param array $data * * @return array */ public function convert(array $data); }
As you can see above, we added the convert
method to our CustomerDataConverterInterface
. There’s no code yet, but we created an extensive PHPDoc for it. That’s because our interface acts as a contract. We need it to be as detailed as possible.
The contract states that the convert
method expect a data array. The method will then convert that data array into an array of customers. And that array of customers should also follow a specific format.
We made that format pretty simple. It’s just an associative array with three keys: name
, email
and details
. These are the three arguments that we want to pass to the add_customer
method.
class CustomerImporter { // ... /** * Import a customer data file. * * @param string $filename * * @return bool|WP_Error */ public function import_from_file($filename) { if (!is_readable($filename)) { return new WP_Error('file_unreadable', sprintf('Unable to read "%" customer data file.', $filename)); } $customer_data = file($filename, FILE_IGNORE_NEW_LINES); if (!is_array($customer_data)) { return new WP_Error('parse_error', sprintf('Failed to parse the "%" customer data file.', $filename)); } $customers = $this->converter->convert($customer_data); // ... return true; } }
Next, we updated our import_from_file
method in the CustomerImporter
class. We added a call to the convert
method of our converter
object. It converts our customer_data
into customers
.
This one line of code is the most important one in all our example. It’s where all the components of the strategy pattern meet. We have a context (CustomerImporter
) using a concrete strategy (converter
object) to perform an operation defined by a strategy interface(CustomerDataConverterInterface
).
Saving our customers
There’s only one last thing left to do in our CustomerImporter
class. It’s saving the customers that we converted from the customer_data
variable. This is pretty easy to do because we’ve done most of the legwork already!
class CustomerImporter { // ... /** * Import a customer data file. * * @param string $filename * * @return bool|WP_Error */ public function import_from_file($filename) { if (!is_readable($filename)) { return new WP_Error('file_unreadable', sprintf('Unable to read "%" customer data file.', $filename)); } $customer_data = file($filename, FILE_IGNORE_NEW_LINES); if (!is_array($customer_data)) { return new WP_Error('parse_error', sprintf('Failed to parse the "%" customer data file.', $filename)); } $customers = $this->converter->convert($customer_data); foreach ($customers as $customer) { $this->add_customer($customer['name'], $customer['email'], $customer['details']); } return true; } }
The code sample above now contains our completed import_from_file
method. The only thing missing was a foreach
loop. It loops through all our customers and passes them to the add_customer
method created earlier.
You should recognize the three arguments passed to the add_customer
method. They’re the three key-value pairs defined earlier in our CustomerDataConverterInterface
interface. This is also why we’re not doing any validation to see if the array keys are there. We trust anyone implementing the CustomerDataConverterInterface
interface to follow its contract.
Building a converter
And, speaking of CustomerDataConverterInterface
, we still need a class that implements it. That way we have a concrete strategy that we can use with our context. So let’s finish up this article by creating one!
Importing customers from a CSV file
For this example, we’ll use a CSV file as the source of our customer data. A CSV file is a text file that stores tabular data using commas to separate rows. Here’s a small CSV file with a few customers in it:
Loyd Mortimer,loyd@awesomesoft.io,Product manager Daniella Chavez,dchavez@imaginaryngo.org,CTO Kenneth Phillips,kenneth.phillips@usedcarsforyou.co,Car salesman Dorris Roberts,d@ilovestuff.us,Sales manager
Converting a CSV customer data file
Next, we need to build a class to convert this CSV file into customer arrays. This is going to be our concrete strategy for handling CSV files. Here’s an initial look at it:
class CSVCustomerDataConverter implements CustomerDataConverterInterface { }
We named our class CSVCustomerDataConverter
. It also implements our CustomerDataConverterInterface
strategy interface. But this means that we need to create a convert
method to satisfy the interface contract. Let’s add that now:
class CSVCustomerDataConverter implements CustomerDataConverterInterface { /** * Convert the given CSV data array into an array of customers. The returned * array should use the following format: * * @param array $data * * @return array */ public function convert(array $data) { // .. } }
Above is our empty convert
method. As a reminder, the data
array passed to the convert
method is an array containing all the lines in our file. In this scenario, that means each array value contains a row from our CSV file.
This makes it easy to convert our file to customer arrays. We just need to loop through all the values in our data
array. On top of that, PHP has a handy function to convert a CSV row into an array.
class CSVCustomerDataConverter implements CustomerDataConverterInterface { /** * Convert the given CSV data array into an array of customers. The returned * array should use the following format: * * @param array $data * * @return array */ public function convert(array $data) { $customers = array(); foreach ($data as $row) { $row = str_getcsv($row); $customers[] = array( 'name' => $row[0], 'email' => $row[1], 'details' => $row[2], ); } return $customers; } }
And now, this is the complete convert
method. First, we start by creating a customers
array. This is the array containing all the customers that the convert
method will create.
We then loop through each row from our data
array. We then pass that row
variable to the str_getcsv
function. This is the PHP function that converts a CSV row string to an array.
The array created by str_getcsv
follows a simple format. Each row column corresponds to an array index starting at 0
. This means that the customer’s name is at index 0
, their email at index 1
and their details at index 2
.
Knowing this, we can create a customer data array. We can also ensure that it follows the convention defined earlier. We then append this customer data array to our larger customers
array. And once through the loop, we return the customers
array back!
Let’s recap
At this point, we’ve finished building the customer importer for our imaginary plugin. Thanks to the strategy pattern, we found an elegant way to separate the different jobs that it had to do. Let’s review how all these different pieces fit together.
First, we created the CustomerImporter
class which will act as the context of the strategy. It handles most of the work importing new customers for our plugin. But, that said, we didn’t let it do everything.
We then extracted the most variable part of the CustomerImporter
class. It’s the part in charge of the conversion of our customer data. We created the CustomerDataConverterInterface
as a strategy interface to represent this variable behaviour.
And, as the final step, we built a concrete strategy that implements this strategy interface. This was the CSVCustomerDataConverter
class. It converted the data from a CSV file into customer data that CustomerImporter
could use.
Just one example
We mentioned earlier that we made this example simple so you could see how the strategy pattern works. But converting data isn’t always the only variable part in an importer. For example, you could want your importer to work with other data sources than just a file.
This another problem where you could use the strategy pattern. But there are plenty of others too. That’s why it’s such a useful pattern to know!
You can find all the code for this article here.