restore_current_blog() vs. multiple switch_to_blog() followed by removing $GLOBALS[‘_wp_switched_stack’]

The question:

Related to this answer, which states

After every instance of switch_to_blog() you need to call restore_current_blog() otherwise WP will think it is in a “switched” mode and can potentially return incorrect data.

I’ve done some testing, and can confirm that this is an issue. (It also answers a question I’ve had at work regarding file upload URLs, so two big thumbs up for that.)

In my testing (detailed below) I found that there were two ways to proceed:

  1. Always pair switch_to_blog() with restore_current_blog()
  2. Use a chain of switch_to_blog(), with the last one switching back to the blog where you started from, and then unset( $GLOBALS['_wp_switched_stack'] ); at the end.

My question: Is method #2 in keeping with WordPress development best practices, or is it an ugly hack?

My Testing

I added the following code to functions.php on one of my WordPress Multisite installations:

add_action( 'shutdown', 'pj_stb_test' );
function pj_stb_test() {
    if( ! current_user_can( 'update_core' ) )
        return;

    $home = get_current_blog_id();
    $checklist = array(
        'constants' => array( 'UPLOADS', 'MULTISITE', 'BLOGUPLOADDIR' ),
        'functions' => array( 'ms_is_switched', 'wp_upload_dir' ),
    );
    $site_ids = array( 1, 2, 3 );

    echo( "switch_to_blog() chain:<br />" );
    foreach( $site_ids as $id ) {
        switch_to_blog( $id );
    }
    switch_to_blog( $home );
    _pj_dump( $checklist );

    echo( 'switch_to_blog() chain followed by 
           unset( $GLOBALS['_wp_switched_stack'] )<br />' );
    foreach( $site_ids as $id ) {
        switch_to_blog( $id );
    }
    switch_to_blog( $home );
    unset( $GLOBALS['_wp_switched_stack'] );
    _pj_dump( $checklist );

    echo( 'switch_to_blog() / restore_current_blog() pairings<br />' );
    foreach( $site_ids as $id ) {
        switch_to_blog( $id );
        restore_current_blog();
    }
    _pj_dump( $checklist );

function _pj_dump( $checklist ) {
    $constants = $checklist['constants'];
    $functions = $checklist['functions'];
    echo( "<p>Constants:</p>" );
    echo( "<pre>n" );
    foreach( $constants as $c ) {
        echo( $c . ': ' );
        var_dump( constant( $c ) );
        echo( "n" );
    }
    foreach( $functions as $f ) {
        echo( $f . ': ' );
        var_dump( call_user_func( $f ) );
        echo( "n" );
    }
}

The output that was returned on an arbitrary site in my network:

switch_to_blog() chain:
UPLOADS: string(30) "wp-content/blogs.dir/94/files/"
MULTISITE: bool(true)
BLOGUPLOADDIR: string(50) "/path/to/wp/wp-content/blogs.dir/94/files/"
ms_is_switched: bool(true)
wp_upload_dir: array(6) {
  ["path"]=>
  string(57) "/path/to/wp/wp-content/blogs.dir/94/files/2013/11"
  ["url"]=>
  string(74) "http://example.com/my-site/wp-content/blogs.dir/94/files/2013/11"
  ["subdir"]=>
  string(8) "/2013/11"
  ["basedir"]=>
  string(49) "/path/to/wp/wp-content/blogs.dir/94/files"
  ["baseurl"]=>
  string(66) "http://example.com/my-site/wp-content/blogs.dir/94/files"
  ["error"]=>
  bool(false)
} 

switch_to_blog() chain followed by unset( $GLOBALS['_wp_switched_stack'] )
UPLOADS: string(30) "wp-content/blogs.dir/94/files/"
MULTISITE: bool(true)
BLOGUPLOADDIR: string(50) "/path/to/wp/wp-content/blogs.dir/94/files/"
ms_is_switched: bool(false)
wp_upload_dir: array(6) {
  ["path"]=>
  string(57) "/path/to/wp/wp-content/blogs.dir/94/files/2013/11"
  ["url"]=>
  string(50) "http://example.com/my-site/files/2013/11"
  ["subdir"]=>
  string(8) "/2013/11"
  ["basedir"]=>
  string(49) "/path/to/wp/wp-content/blogs.dir/94/files"
  ["baseurl"]=>
  string(42) "http://example.com/my-site/files"
  ["error"]=>
  bool(false)
} 

switch_to_blog() / restore_current_blog() pairings
UPLOADS: string(30) "wp-content/blogs.dir/94/files/"
MULTISITE: bool(true)
BLOGUPLOADDIR: string(50) "/path/to/wp/wp-content/blogs.dir/94/files/"
ms_is_switched: bool(false)
wp_upload_dir: array(6) {
  ["path"]=>
  string(57) "/path/to/wp/wp-content/blogs.dir/94/files/2013/11"
  ["url"]=>
  string(50) "http://example.com/my-site/files/2013/11"
  ["subdir"]=>
  string(8) "/2013/11"
  ["basedir"]=>
  string(49) "/path/to/wp/wp-content/blogs.dir/94/files"
  ["baseurl"]=>
  string(42) "http://example.com/my-site/files"
  ["error"]=>
  bool(false)
}

Sorry for the verbosity, but I wanted to make sure I had everything in here.

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

Short answer is NO. That is an ugly hack =) More global variables are affect than the ones that you are var_dump’ing. Here are the globals that I found affected: $wpdb, $wp_roles, $wp_object_cache, $global_groups, $GLOBALS[‘_wp_switched_stack’], $GLOBALS[‘blog_id’], and $GLOBALS[‘table_prefix’]. There could be more. I found that $wpdb was another major variable that was altered by switch_to_blog(). Try var_dump’ing $wpdb in your test and you will see the effect. Any APIs relying on those globals could be affected if you do not “Always pair switch_to_blog() with restore_current_blog()”

Long answer is “depends on the situation.” I would say the statement “After every instance of switch_to_blog() you need to call restore_current_blog()” is the best practice and generally true. But there are situations where you do not need to return to the originial blog or do restore_current_blog(). For example, I created an admin plugin which altered a users role across all blogs. It iterated through all the blogs in the network using switch_to_blog, called other WP APIs (that did not rely on those globals), and ended immediately. Hence no need to restore_current_blog(). YMMV depending on where and how switch_to_blog() is used.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment