I'm migrating about 500 posts (WooCommerce products, actually) to a new Custom Post type (using Convert Post Types plugin) and want to also migrate the Categories.

There are the Taxonomy Converter plugin which migrates the categories themselves, but not their hierarchies, though it says it will.

There's also simply running:

UPDATE wp_term_taxonomy SET taxonomy='project-category' WHERE taxonomy='product_cat';

I can manually adjust the hierarchy, which is what I'm leaning toward at this point, but I came across some code here from a number of years ago, which I updated a bit and would love to see work. It's included below.

Each row returned by the Recursive Iterator looks something like this:

    [term_id] => 39
    [description] => Bla bla bla
    [parent] => 0
    [count] => 7
    [name] => Security Bars
    [slug] => security-bars

But I'm fuzzy on where that information wants to be inserted. I think something along the lines of:

$qry = $wpdb->insert('wp_term_taxonomy', array(
                            'term_id' => $row['term_id'],
                            'taxonomy' => 'project-category',
                            'description' => $row['description'],
                            'parent' => $parent_id

But that still isn't giving me hierarchy. I think this is because each parent needs to be the ID some a wp_term_taxonomy I have just created.

Any suggestions? A plugin that does this would be welcome.

Code Thus Far:

class RecursiveCategoryIterator implements RecursiveIterator {
    const ID_FIELD = 'term_id';
    const PARENT_FIELD = 'parent';

    private $_data;
    private $_root;
    private $_position = 0;

    public function __construct(array $data, $root_id = 0) {
        $this->_data = $data;
        $this->_root = $root_id;

    public function valid() {
        return isset($this->_data[$this->_root][$this->_position]);

    public function hasChildren() {
        $subid = $this->_data[$this->_root][$this->_position][self::ID_FIELD];
        return isset($this->_data[$subid])
            && is_array($this->_data[$subid]);

    public function next() {

    public function current() {
        return $this->_data[$this->_root][$this->_position];

    public function getChildren() {
        return new self($this->_data,

    public function rewind() {
        $this->_position = 0;

    public function key() {
        return $this->_position;

    public static function createFromResult($result) {
        $menu_array = array();
        foreach($result as $row) {
            $menu_array[$row[self::PARENT_FIELD]][] = $row;

        return new self($menu_array);

The function that makes use of it:

function migrate_terms() {

    $sql="SELECT a.term_id,a.description,a.parent,a.count,b.name,b.slug
    FROM wp_term_taxonomy a INNER JOIN wp_terms b WHERE a.term_id=b.term_id
    AND a.taxonomy='product_cat';
    global $wpdb;
    $result = $wpdb->get_results($sql, ARRAY_A);

    // always test for failure
    if($result === false) {
        die("query failed: ". $wpdb->show_errors() );

    // create the iterator from the result set
    $wpterms = RecursiveCategoryIterator::createFromResult($result);
        // Look at it
        echo "<pre>";
        echo "</pre>";
    // and import it. 
    //insert_it($wpterms, 0);


And the "function which does all the dirty work":

function insert_it($iterator, $parent_id = 0) {
    foreach($iterator as $row) {
        // insert the row, just edit the query, and don't forget
        // to escape the values. if you have an insert function,
        // use it by all means
        $qry = $wpdb->insert(WHAT GOES HERE?);

        $status = $wpdb->get_results($qry);

        if($status === false) {
            // insert failed - rollback and abort
            die("hard: " . $wpdb->show_errors() );

        // you need to pass the id of the new row
        // so the "child rows" have their respective parent
        $cid = $wpdb->insert_id;

        // insert the children too
        if($iterator->hasChildren()) {
            insert_it($iterator->getChildren(), $cid);

// Now we set that function up to execute when the admin_notices action is called
add_action( 'admin_notices', 'migrate_terms' );

So that at least there's a solution here, will add that this plugin seems to succesfully migrate Posts as well as category and hierarchy.: https://wordpress.org/plugins/post-type-convertr.

On the production server, I also needed to run the following SQL query after migrating with the Post Type Convertr plugin:

UPDATE wp_term_taxonomy SET taxonomy='project-category' WHERE taxonomy='product_cat';

I dunno. Sorry it's not more useful information.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.