A Better Way to Import WordPress Data: The Strategy Pattern

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 potentially breaking another. It’s a classic rookie mistake.

The WordPress Importer Strategy Pattern Explained

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 that 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 if 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 new formats means adding new, small classes, not modifying a giant one.
  • It’s Testable: You can write unit tests for each converter in isolation without needing the whole importer.
  • It’s Maintainable: The code is decoupled. A bug in the pipe-delimited converter won’t break the CSV importer. Trust me on this, that’s a big deal.

Look, 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.

Leave a Reply

Your email address will not be published. Required fields are marked *