I had a client with a new membership site who needed to import thousands of users from two of their old systems. The first system gave us a clean CSV file. Easy enough. But the second? It spat out a pipe-delimited text file with a completely different column order and funky date formats. A total mess. This is a classic case where a simple import job gets complicated, and where you can either build a robust system or paint yourself into a corner. We chose the robust path, using the WordPress importer strategy pattern.
My first thought, honestly, was to just get the CSV import working and then jam an if statement in there to handle the pipe-delimited file. It’s the path of least resistance, right? But after 14 years of this, you learn that the easy way now is often the hard way later. An importer built on if/else blocks for different formats becomes a nightmare to maintain. Add a third data source and the whole thing falls apart. You can’t test one format without breaking another. It’s a classic rookie mistake.
How the WordPress importer strategy pattern works
The core problem is that the job of “saving a user” is always the same, but the job of “reading the data” changes with every source. The strategy pattern lets us separate those two concerns. Instead of one giant class that does everything, we have a main Importer class (the “Context”) that only knows how to save the data once it’s in a standard format. Then we create separate, interchangeable classes (the “Strategies”) for each data source. Each strategy’s only job is to read its specific file format (CSV, TXT, XML, etc.) and convert it into the standard format the Importer expects.
This approach is heavily inspired by the solid principles outlined in an old post from Carl Alexander, which is still relevant today. The main idea is to create a contract, or an interface in PHP, that every data converter strategy must follow. The main importer doesn’t care whether it’s dealing with CSV or something else; it just knows it’s getting an object that fulfills the contract.
<?php
// The "Contract" - Every converter MUST have this method.
interface UserDataConverterInterface {
public function convert(array $data_rows): array;
}
// The "Context" - It only knows how to save users.
class UserImporter {
private $converter;
// We tell the importer WHICH strategy to use.
public function __construct(UserDataConverterInterface $converter) {
$this->converter = $converter;
}
public function import(string $filename) {
$lines = file($filename, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return new WP_Error('file_error', 'Could not read the import file.');
}
// Delegate the conversion to the strategy object.
$users = $this->converter->convert($lines);
foreach ($users as $user) {
// The saving logic is always the same.
$this->save_user($user['name'], $user['email']);
}
return true;
}
private function save_user($name, $email) {
// Your logic for wp_insert_user or updating metadata goes here.
// This part never changes, no matter the data source.
}
}
// A "Concrete Strategy" for CSV files.
class CsvUserDataConverter implements UserDataConverterInterface {
public function convert(array $data_rows): array {
$users = [];
foreach ($data_rows as $row) {
$data = str_getcsv($row);
$users[] = [
'name' => $data[0],
'email' => $data[1],
];
}
return $users;
}
}
So what’s the point?
Look at that structure. The UserImporter is clean. It has one job and does it well. The CsvUserDataConverter is also simple and has only one job. Now, when the client comes back and says they found another database that exports to JSON, what do we do? We don’t touch the existing, working code. Period. We just create a new class called JsonUserDataConverter, make it implement the same interface, and we’re done. It’s predictable, testable, and it won’t give the next developer a headache.
- It’s extensible: adding a new format means a new, small class, not edits to a giant one.
- It’s testable: you can unit test each converter on its own, without the whole importer.
- It’s maintainable: the code is decoupled, so a bug in the pipe-delimited converter won’t break the CSV importer. Trust me on this, that’s a big deal.
This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.