“switch_to_blog( )” is an Unfunny Nightmare

It can be extremely challenging to write WordPress code that works across many environments and allows you to use a custom location for WordPress media, WordPress multisite media, and WordPress core itself. I wrote about this extensively here. The code I included in that post works, although a lot of it was excerpted to be shown as a short example here or there. If your site switches from one blog to the next and doesn’t intermingle content – you really don’t have much to worry about after your initial setup. But in almost all cases, if you want to start using switch_to_blog() to co-mingle content from multiple sites inline, get ready to do some debugging!

switch_to_blog() works like so:

// I am on my blog

switch_to_blog( 2 );

// I am on your mom's blog

restore_current_blog();

// I am on my blog

Simple enough. This will switch context all over the place in WP core. wp_posts will become wp_2_posts. get_current_blog_id() will return 2 because the global variable $blog_id will be set to 2, etc.

In my post about environments, I had a lot of filters that I suggested adding in wp-content/sunrise.php that work just great in switching initial context to a specific blog and in setting up overrides for default media upload paths etc. They work fine… unless you ever plan on using switch_to_blog().

Here’s an example:

// Before
add_filter( 'pre_option_home', function ( $str ) use ( $domain ) {
    return 'http://' . $domain;
} );

// After using switch_to_blog and realizing I needed to account
// for any sort of weird context I may find myself in

add_filter( 'pre_option_home', function () {
    global $current_blog;
    $extra = rtrim( $current_blog->path, '/' );
    return 'http://' . MY_ENVIRONMENT_HOST . $extra;
} );

Ok cool, so you pull a path from a global variable and append it if it still has a value after being rtrim‘d? Easy. That would be true if $current_blog and its properties were updated every time switch_to_blog() is called. It is not!

$current_blog is one of the values we set in wp-content/sunrise.php to override the database and set our $current_blog->domain value to our current environment. $current_blog and $current_site exist mainly for use on the initialization of Multisite. Outside of startup, they aren’t really accessed or modified.

Because I want a static way to access dynamic variables, I have added some code to change the context of $current_blog when switch_to_blog() is called in wp-content/sunrise.php:

function emusic_switch_to_blog( $blog_id, $prev_blog_id = 0 ) {
    if ( $blog_id === $prev_blog_id )
        return;

    global $current_blog, $wpdb;
    $current_blog = $wpdb->get_row( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = {$blog_id} LIMIT 1" );
    $current_blog->domain = MY_ENVIRONMENT_HOST;
}

emusic_switch_to_blog( $the_id );

add_action( 'switch_blog', 'emusic_switch_to_blog', 10, 2 );

Now that I have added that action, my filter pre_option_home will work. I use the same method for pre_option_siteurl. What I was previously doing to retrieve the path of the current blog didn’t work:

add_filter( 'pre_option_siteurl', function () {
    $extra = rtrim( get_blog_details( get_current_blog_id() )->path, '/' );
    return 'http://' . EMUSIC_CURRENT_HOST . $extra;
} );

Why didn’t it work? get_blog_details() eventually does $details->siteurl = get_blog_option( $blog_id, 'siteurl' ); giving us a nice and hardy dose of infinite recursion. So to combat it – I implemented the setting of $current_blog on the switch_blog action so its properties are always the current blog’s properties. Boom.

The next 2 annoyances are media upload urls / paths and admin paths. We use custom media locations for the main blog and network blogs:

add_filter( 'pre_option_upload_path', function () {
    $id = get_current_blog_id();
    if ( 1 < $id )
        return $_SERVER['DOCUMENT_ROOT'] . "/blogs/{$id}/files";

     return $_SERVER['DOCUMENT_ROOT'] . '/' . EMUSIC_UPLOADS;
} );

add_filter( 'pre_option_upload_url_path', function () {
    $id = get_current_blog_id();
    if ( 1 < $id )
        return 'http://' . EMUSIC_CURRENT_HOST . "/blogs/{$id}/files";

     return 'http://' . EMUSIC_CURRENT_HOST  . '/' . EMUSIC_UPLOADS;
} );

We switched blog context using switch_to_blog(), which triggers our action, which then sets the global variable $blog_id to our current blog’s id, which can then be retrieved from within functions by get_current_blog_id(). Yeah, that’s a mouthful. We are also using a constant for our host name, so we don’t have to query for it / output-buffer it / or str_replace() it.

The admin is a little bit trickier because we usually don’t call switch_to_blog() in the code, but the admin bar will list your blogs and has URLs to their Dashboards etc. We can filter those as well:

function get_admin_host( $url, $path, $blog_id = '' ) {
    $path = ltrim( $path, '/' );

    if ( empty( $blog_id ) ) {
        $blog_id = get_current_blog_id();
    }

    $blog_path = rtrim( get_blog_details( $blog_id )->path, '/' );
    return sprintf( 'http://%s%s/wp-admin/%s', EMUSIC_CURRENT_HOST, $blog_path, $path );
}

add_filter( 'admin_url', 'get_admin_host', 10, 3 );

function get_network_admin_host( $url, $path, $blog_id = '' ) {
    $path = ltrim( $path, '/' );

    if ( empty( $blog_id ) ) {
        $blog_id = get_current_blog_id();
    }

    $blog_path = rtrim( get_blog_details( $blog_id )->path, '/' );
    return sprintf( 'http://%s%s/wp-admin/network/%s', EMUSIC_CURRENT_HOST, $blog_path, $path );
}

add_filter( 'network_admin_url', 'get_network_admin_host', 10, 3 );

If you want to see this in action, I recently integrated the eMusic editors’ 17 Dots blog into eMusic proper, check it out: 17 Dots. You can see multiple blogs intertwining on the homepage.